Delete groups (legacy communities system) (#8027)
* Remove deprecated feature_communities_v2_prototypes * Update _components * i18n * delint * Cut out a bit more dead code * Carve into legacy components * Carve into mostly the room list code * Carve into instances of "groupId" * Carve out more of what comes up with "groups" * Carve out some settings * ignore related groups state * Remove instances of spacesEnabled * Fix some obvious issues * Remove now-unused css * Fix variable naming for legacy components * Update i18n * Misc cleanup from manual review * Update snapshot for changed flag * Appease linters * rethemedex * Remove now-unused AddressPickerDialog * Make ConfirmUserActionDialog's member a required prop * Remove useless override from RightPanelStore * Remove extraneous CSS * Update i18n * Demo: "Communities are now Spaces" landing page * Restore linkify for group IDs * Demo: Dialog on click for communities->spaces notice * i18n for demos * i18n post-merge * Update copy * Appease the linter * Post-merge cleanup * Re-add spaces_learn_more_url to the new SdkConfig place * Round 1 of post-merge fixes * i18n Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
03c80707c9
commit
fce36ec826
171 changed files with 317 additions and 12160 deletions
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
Copyright 2017, 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 { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||
|
||||
import BaseAvatar from './BaseAvatar';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
export interface IProps {
|
||||
groupId?: string;
|
||||
groupName?: string;
|
||||
groupAvatarUrl?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
resizeMethod?: ResizeMethod;
|
||||
onClick?: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.avatars.GroupAvatar")
|
||||
export default class GroupAvatar extends React.Component<IProps> {
|
||||
public static defaultProps = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
resizeMethod: 'crop',
|
||||
};
|
||||
|
||||
getGroupAvatarUrl() {
|
||||
if (!this.props.groupAvatarUrl) return null;
|
||||
return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
|
||||
this.props.width,
|
||||
this.props.height,
|
||||
this.props.resizeMethod,
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
// extract the props we use from props so we can pass any others through
|
||||
// should consider adding this as a global rule in js-sdk?
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const { groupId, groupAvatarUrl, groupName, ...otherProps } = this.props;
|
||||
|
||||
return (
|
||||
<BaseAvatar
|
||||
name={groupName || this.props.groupId[1]}
|
||||
idName={this.props.groupId}
|
||||
url={this.getGroupAvatarUrl()}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Group } from 'matrix-js-sdk/src/models/group';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import { MenuItem } from "../../structures/ContextMenu";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.context_menus.GroupInviteTileContextMenu")
|
||||
export default class GroupInviteTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
group: PropTypes.instanceOf(Group).isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onClickReject = this._onClickReject.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onClickReject() {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
|
||||
title: _t('Reject invitation'),
|
||||
description: _t('Are you sure you want to reject the invitation?'),
|
||||
onFinished: async (shouldLeave) => {
|
||||
if (!shouldLeave) return;
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
try {
|
||||
await GroupStore.leaveGroup(this.props.group.groupId);
|
||||
} catch (e) {
|
||||
logger.error("Error rejecting community invite: ", e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Unable to reject invite"),
|
||||
});
|
||||
} finally {
|
||||
modal.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject}>
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg").default} width="15" height="15" alt="" />
|
||||
{ _t('Reject') }
|
||||
</MenuItem>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||
import { MenuItem } from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
import { createSpaceFromCommunity } from "../../../utils/space";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
|
||||
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
||||
export default class TagTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
tag: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
_onViewCommunityClick = () => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.tag,
|
||||
});
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onRemoveClick = () => {
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onCreateSpaceClick = () => {
|
||||
createSpaceFromCommunity(this.context, this.props.tag);
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onMoveUp = () => {
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onMoveDown = () => {
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
|
||||
this.props.onFinished();
|
||||
};
|
||||
|
||||
render() {
|
||||
let moveUp;
|
||||
let moveDown;
|
||||
if (this.props.index > 0) {
|
||||
moveUp = (
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveUp" onClick={this._onMoveUp}>
|
||||
{ _t("Move up") }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
|
||||
moveDown = (
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveDown" onClick={this._onMoveDown}>
|
||||
{ _t("Move down") }
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
let createSpaceOption;
|
||||
if (GroupStore.isUserPrivileged(this.props.tag)) {
|
||||
createSpaceOption = <>
|
||||
<hr className="mx_TagTileContextMenu_separator" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_createSpace" onClick={this._onCreateSpaceClick}>
|
||||
{ _t("Create Space") }
|
||||
</MenuItem>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||
{ _t('View Community') }
|
||||
</MenuItem>
|
||||
{ (moveUp || moveDown) ? <hr className="mx_TagTileContextMenu_separator" /> : null }
|
||||
{ moveUp }
|
||||
{ moveDown }
|
||||
<hr className="mx_TagTileContextMenu_separator" />
|
||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||
{ _t("Unpin") }
|
||||
</MenuItem>
|
||||
{ createSpaceOption }
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,759 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { AddressType, addressTypes, getAddressType, IUserAddress } from '../../../UserAddress';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from '../../../email';
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
|
||||
import { abbreviateUrl } from '../../../utils/UrlUtils';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AddressSelector from '../elements/AddressSelector';
|
||||
import AddressTile from '../elements/AddressTile';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
const addressTypeName = {
|
||||
'mx-user-id': _td("Matrix ID"),
|
||||
'mx-room-id': _td("Matrix Room ID"),
|
||||
'email': _td("email address"),
|
||||
};
|
||||
|
||||
interface IResult {
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
room_id?: string; // eslint-disable-line camelcase
|
||||
name?: string;
|
||||
display_name?: string; // eslint-disable-line camelcase
|
||||
avatar_url?: string;// eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
description?: JSX.Element;
|
||||
// Extra node inserted after picker input, dropdown and errors
|
||||
extraNode?: JSX.Element;
|
||||
value?: string;
|
||||
placeholder?: ((validAddressTypes: any) => string) | string;
|
||||
roomId?: string;
|
||||
button?: string;
|
||||
focus?: boolean;
|
||||
validAddressTypes?: AddressType[];
|
||||
onFinished: (success: boolean, list?: IUserAddress[]) => void;
|
||||
groupId?: string;
|
||||
// The type of entity to search for. Default: 'user'.
|
||||
pickerType?: 'user' | 'room';
|
||||
// Whether the current user should be included in the addresses returned. Only
|
||||
// applicable when pickerType is `user`. Default: false.
|
||||
includeSelf?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// Whether to show an error message because of an invalid address
|
||||
invalidAddressError: boolean;
|
||||
// List of UserAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
selectedList: IUserAddress[];
|
||||
// Whether a search is ongoing
|
||||
busy: boolean;
|
||||
// An error message generated during the user directory search
|
||||
searchError: string;
|
||||
// Whether the server supports the user_directory API
|
||||
serverSupportsUserDirectory: boolean;
|
||||
// The query being searched for
|
||||
query: string;
|
||||
// List of UserAddressType objects representing the set of
|
||||
// auto-completion results for the current search query.
|
||||
suggestedList: IUserAddress[];
|
||||
// List of address types initialised from props, but may change while the
|
||||
// dialog is open and represents the supported list of address types at this time.
|
||||
validAddressTypes: AddressType[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.AddressPickerDialog")
|
||||
export default class AddressPickerDialog extends React.Component<IProps, IState> {
|
||||
private textinput = createRef<HTMLTextAreaElement>();
|
||||
private addressSelector = createRef<AddressSelector>();
|
||||
private queryChangedDebouncer: number;
|
||||
private cancelThreepidLookup: () => void;
|
||||
|
||||
static defaultProps: Partial<IProps> = {
|
||||
value: "",
|
||||
focus: true,
|
||||
validAddressTypes: addressTypes,
|
||||
pickerType: 'user',
|
||||
includeSelf: false,
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
let validAddressTypes = this.props.validAddressTypes;
|
||||
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
|
||||
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes(AddressType.Email)) {
|
||||
validAddressTypes = validAddressTypes.filter(type => type !== AddressType.Email);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
invalidAddressError: false,
|
||||
selectedList: [],
|
||||
busy: false,
|
||||
searchError: null,
|
||||
serverSupportsUserDirectory: true,
|
||||
query: "",
|
||||
suggestedList: [],
|
||||
validAddressTypes,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
this.textinput.current.value = this.props.value;
|
||||
}
|
||||
}
|
||||
|
||||
private getPlaceholder(): string {
|
||||
const { placeholder } = this.props;
|
||||
if (typeof placeholder === "string") {
|
||||
return placeholder;
|
||||
}
|
||||
// Otherwise it's a function, as checked by prop types.
|
||||
return placeholder(this.state.validAddressTypes);
|
||||
}
|
||||
|
||||
private onButtonClick = (): void => {
|
||||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.textinput.current.value !== '') {
|
||||
selectedList = this.addAddressesToList([this.textinput.current.value]);
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, selectedList);
|
||||
};
|
||||
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onKeyDown = (e: React.KeyboardEvent): void => {
|
||||
const textInput = this.textinput.current ? this.textinput.current.value : undefined;
|
||||
let handled = true;
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
||||
|
||||
if (action === KeyBindingAction.Escape) {
|
||||
this.props.onFinished(false);
|
||||
} else if (e.key === KeyBindingAction.ArrowUp) {
|
||||
this.addressSelector.current?.moveSelectionUp();
|
||||
} else if (e.key === KeyBindingAction.ArrowDown) {
|
||||
this.addressSelector.current?.moveSelectionDown();
|
||||
} else if (
|
||||
[KeyBindingAction.Comma, KeyBindingAction.Enter, KeyBindingAction.Tab].includes(action) &&
|
||||
this.state.suggestedList.length > 0
|
||||
) {
|
||||
this.addressSelector.current?.chooseSelection();
|
||||
} else if (textInput.length === 0 && this.state.selectedList.length && action === KeyBindingAction.Backspace) {
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.key === KeyBindingAction.Enter) {
|
||||
if (textInput === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
this.addAddressesToList([textInput]);
|
||||
}
|
||||
} else if (textInput && [KeyBindingAction.Comma, KeyBindingAction.Tab].includes(action)) {
|
||||
this.addAddressesToList([textInput]);
|
||||
} else {
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onQueryChanged = (ev: React.ChangeEvent): void => {
|
||||
const query = (ev.target as HTMLTextAreaElement).value;
|
||||
if (this.queryChangedDebouncer) {
|
||||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query !== '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.props.pickerType === 'user') {
|
||||
if (this.props.groupId) {
|
||||
this.doNaiveGroupSearch(query);
|
||||
} else if (this.state.serverSupportsUserDirectory) {
|
||||
this.doUserDirectorySearch(query);
|
||||
} else {
|
||||
this.doLocalSearch(query);
|
||||
}
|
||||
} else if (this.props.pickerType === 'room') {
|
||||
if (this.props.groupId) {
|
||||
this.doNaiveGroupRoomSearch(query);
|
||||
} else {
|
||||
this.doRoomSearch(query);
|
||||
}
|
||||
} else {
|
||||
logger.error('Unknown pickerType', this.props.pickerType);
|
||||
}
|
||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
searchError: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onDismissed = (index: number) => () => {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.splice(index, 1);
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||
};
|
||||
|
||||
private onSelected = (index: number): void => {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(this.getFilteredSuggestions()[index]);
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||
};
|
||||
|
||||
private doNaiveGroupSearch(query: string): void {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().getGroupUsers(this.props.groupId).then((resp) => {
|
||||
const results = [];
|
||||
resp.chunk.forEach((u) => {
|
||||
const userIdMatch = u.user_id.toLowerCase().includes(lowerCaseQuery);
|
||||
const displayNameMatch = (u.displayname || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(userIdMatch || displayNameMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
user_id: u.user_id,
|
||||
avatar_url: u.avatar_url,
|
||||
display_name: u.displayname,
|
||||
});
|
||||
});
|
||||
this.processResults(results, query);
|
||||
}).catch((err) => {
|
||||
logger.error('Error whilst searching group rooms: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doNaiveGroupRoomSearch(query: string): void {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const results = [];
|
||||
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
|
||||
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
room_id: r.room_id,
|
||||
avatar_url: r.avatar_url,
|
||||
name: r.name || r.canonical_alias,
|
||||
});
|
||||
});
|
||||
this.processResults(results, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
|
||||
private doRoomSearch(query: string): void {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
const results = [];
|
||||
rooms.forEach((room) => {
|
||||
let rank = Infinity;
|
||||
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
||||
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||
const canonicalAlias = room.getCanonicalAlias();
|
||||
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
|
||||
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
|
||||
return a.concat(b);
|
||||
}, []);
|
||||
|
||||
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
let aliasMatch = false;
|
||||
let shortestMatchingAliasLength = Infinity;
|
||||
aliases.forEach((alias) => {
|
||||
if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
|
||||
aliasMatch = true;
|
||||
if (shortestMatchingAliasLength > alias.length) {
|
||||
shortestMatchingAliasLength = alias.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!(nameMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aliasMatch) {
|
||||
// A shorter matching alias will give a better rank
|
||||
rank = shortestMatchingAliasLength;
|
||||
}
|
||||
|
||||
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||
|
||||
results.push({
|
||||
rank,
|
||||
room_id: room.roomId,
|
||||
avatar_url: avatarUrl,
|
||||
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by rank ascending (a high rank being less relevant)
|
||||
const sortedResults = results.sort((a, b) => {
|
||||
return a.rank - b.rank;
|
||||
});
|
||||
|
||||
this.processResults(sortedResults, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
|
||||
private doUserDirectorySearch(query: string): void {
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().searchUserDirectory({
|
||||
term: query,
|
||||
}).then((resp) => {
|
||||
// The query might have changed since we sent the request, so ignore
|
||||
// responses for anything other than the latest query.
|
||||
if (this.state.query !== query) {
|
||||
return;
|
||||
}
|
||||
this.processResults(resp.results, query);
|
||||
}).catch((err) => {
|
||||
logger.error('Error whilst searching user directory: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
if (err.errcode === 'M_UNRECOGNIZED') {
|
||||
this.setState({
|
||||
serverSupportsUserDirectory: false,
|
||||
});
|
||||
// Do a local search immediately
|
||||
this.doLocalSearch(query);
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doLocalSearch(query: string): void {
|
||||
this.setState({
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
const queryLowercase = query.toLowerCase();
|
||||
const results = [];
|
||||
MatrixClientPeg.get().getUsers().forEach((user) => {
|
||||
if (user.userId.toLowerCase().indexOf(queryLowercase) === -1 &&
|
||||
user.displayName.toLowerCase().indexOf(queryLowercase) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Put results in the format of the new API
|
||||
results.push({
|
||||
user_id: user.userId,
|
||||
display_name: user.displayName,
|
||||
avatar_url: user.avatarUrl,
|
||||
});
|
||||
});
|
||||
this.processResults(results, query);
|
||||
}
|
||||
|
||||
private processResults(results: IResult[], query: string): void {
|
||||
const suggestedList = [];
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(result.room_id);
|
||||
if (room) {
|
||||
const tombstone = room.currentState.getStateEvents('m.room.tombstone', '');
|
||||
if (tombstone && tombstone.getContent() && tombstone.getContent()["replacement_room"]) {
|
||||
const replacementRoom = client.getRoom(tombstone.getContent()["replacement_room"]);
|
||||
|
||||
// Skip rooms with tombstones where we are also aware of the replacement room.
|
||||
if (replacementRoom) return;
|
||||
}
|
||||
}
|
||||
suggestedList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.props.includeSelf &&
|
||||
result.user_id === MatrixClientPeg.get().credentials.userId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
suggestedList.push({
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
});
|
||||
|
||||
// If the query is a valid address, add an entry for that
|
||||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (this.state.validAddressTypes.includes(addrType)) {
|
||||
if (addrType === 'email' && !Email.looksValid(query)) {
|
||||
this.setState({ searchError: _t("That doesn't look like a valid email address") });
|
||||
return;
|
||||
}
|
||||
suggestedList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||
if (addrType === 'email') {
|
||||
this.lookupThreepid(addrType, query);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
suggestedList,
|
||||
invalidAddressError: false,
|
||||
}, () => {
|
||||
if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop();
|
||||
});
|
||||
}
|
||||
|
||||
private addAddressesToList(addressTexts: string[]): IUserAddress[] {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
|
||||
let hasError = false;
|
||||
addressTexts.forEach((addressText) => {
|
||||
addressText = addressText.trim();
|
||||
const addrType = getAddressType(addressText);
|
||||
const addrObj: IUserAddress = {
|
||||
addressType: addrType,
|
||||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
|
||||
if (!this.state.validAddressTypes.includes(addrType)) {
|
||||
hasError = true;
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType === 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
}
|
||||
|
||||
selectedList.push(addrObj);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
invalidAddressError: hasError ? true : this.state.invalidAddressError,
|
||||
});
|
||||
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||
return hasError ? null : selectedList;
|
||||
}
|
||||
|
||||
private async lookupThreepid(medium: AddressType, address: string): Promise<string> {
|
||||
let cancelled = false;
|
||||
// Note that we can't safely remove this after we're done
|
||||
// because we don't know that it's the same one, so we just
|
||||
// leave it: it's replacing the old one each time so it's
|
||||
// not like they leak.
|
||||
this.cancelThreepidLookup = function() {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
// wait a bit to let the user finish typing
|
||||
await sleep(500);
|
||||
if (cancelled) return null;
|
||||
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = await authClient.getAccessToken();
|
||||
if (cancelled) return null;
|
||||
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||
medium,
|
||||
address,
|
||||
undefined /* callback */,
|
||||
identityAccessToken,
|
||||
);
|
||||
if (cancelled || lookup === null || !lookup.mxid) return null;
|
||||
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||
if (cancelled || profile === null) return null;
|
||||
|
||||
this.setState({
|
||||
suggestedList: [{
|
||||
// a UserAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
displayName: profile.displayname,
|
||||
avatarMxc: profile.avatar_url,
|
||||
isKnown: true,
|
||||
}],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
this.setState({
|
||||
searchError: _t('Something went wrong!'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getFilteredSuggestions(): IUserAddress[] {
|
||||
// map addressType => set of addresses to avoid O(n*m) operation
|
||||
const selectedAddresses = {};
|
||||
this.state.selectedList.forEach(({ address, addressType }) => {
|
||||
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
|
||||
selectedAddresses[addressType].add(address);
|
||||
});
|
||||
|
||||
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||
return this.state.suggestedList.filter(({ address, addressType }) => {
|
||||
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||
});
|
||||
}
|
||||
|
||||
private onPaste = (e: React.ClipboardEvent): void => {
|
||||
// Prevent the text being pasted into the textarea
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData("text");
|
||||
// Process it as a list of addresses to add instead
|
||||
this.addAddressesToList(text.split(/[\s,]+/));
|
||||
};
|
||||
|
||||
private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
|
||||
// Update the IS in account data. Actually using it may trigger terms.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useDefaultIdentityServer();
|
||||
|
||||
// Add email as a valid address type.
|
||||
const { validAddressTypes } = this.state;
|
||||
validAddressTypes.push(AddressType.Email);
|
||||
this.setState({ validAddressTypes });
|
||||
};
|
||||
|
||||
private onManageSettingsClick = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
this.onCancel();
|
||||
};
|
||||
|
||||
render() {
|
||||
let inputLabel;
|
||||
if (this.props.description) {
|
||||
inputLabel = <div className="mx_AddressPickerDialog_label">
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const query = [];
|
||||
// create the invite list
|
||||
if (this.state.selectedList.length > 0) {
|
||||
for (let i = 0; i < this.state.selectedList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.selectedList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the query at the end
|
||||
query.push(
|
||||
<textarea
|
||||
key={this.state.selectedList.length}
|
||||
onPaste={this.onPaste}
|
||||
rows={1}
|
||||
id="textinput"
|
||||
ref={this.textinput}
|
||||
className="mx_AddressPickerDialog_input"
|
||||
onChange={this.onQueryChanged}
|
||||
placeholder={this.getPlaceholder()}
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}
|
||||
/>,
|
||||
);
|
||||
|
||||
const filteredSuggestedList = this.getFilteredSuggestions();
|
||||
|
||||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.invalidAddressError) {
|
||||
const validTypeDescriptions = this.state.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||
error = <div className="mx_AddressPickerDialog_error">
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
}) }
|
||||
</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_AddressPickerDialog_error">{ this.state.searchError }</div>;
|
||||
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
|
||||
error = <div className="mx_AddressPickerDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={this.addressSelector}
|
||||
addressList={filteredSuggestedList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let identityServer;
|
||||
// If picker cannot currently accept e-mail but should be able to
|
||||
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes(AddressType.Email)
|
||||
&& this.props.validAddressTypes.includes(AddressType.Email)) {
|
||||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||
if (defaultIdentityServerUrl) {
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
{
|
||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => (
|
||||
<AccessibleButton kind="link_inline" onClick={this.onUseDefaultIdentityServerClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>
|
||||
),
|
||||
settings: sub => <AccessibleButton kind="link_inline" onClick={this.onManageSettingsClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
) }</div>;
|
||||
} else {
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <AccessibleButton kind="link_inline" onClick={this.onManageSettingsClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
) }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_AddressPickerDialog"
|
||||
onKeyDown={this.onKeyDown}
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
>
|
||||
{ inputLabel }
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
|
||||
{ error }
|
||||
{ addressSelector }
|
||||
{ this.props.extraNode }
|
||||
{ identityServer }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onButtonClick}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -204,7 +204,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
|||
<p>
|
||||
{ _t(
|
||||
"Debug logs contain application usage data including your " +
|
||||
"username, the IDs or aliases of the rooms or groups you " +
|
||||
"username, the IDs or aliases of the rooms you " +
|
||||
"have visited, which UI elements you last interacted with, " +
|
||||
"and the usernames of other users. They do not contain messages.",
|
||||
) }
|
||||
|
|
|
@ -1,260 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, FormEvent } from 'react';
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import InviteDialog from "./InviteDialog";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import { inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "./ErrorDialog";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
roomId: string;
|
||||
communityName: string;
|
||||
}
|
||||
|
||||
interface IPerson {
|
||||
userId: string;
|
||||
user: RoomMember;
|
||||
lastActive: number;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
emailTargets: string[];
|
||||
userTargets: string[];
|
||||
showPeople: boolean;
|
||||
people: IPerson[];
|
||||
numPeople: number;
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CommunityPrototypeInviteDialog")
|
||||
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
emailTargets: [],
|
||||
userTargets: [],
|
||||
showPeople: false,
|
||||
people: this.buildSuggestions(),
|
||||
numPeople: 5, // arbitrary default
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
private buildSuggestions(): IPerson[] {
|
||||
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get("welcome_user_id")]);
|
||||
if (this.props.roomId) {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
||||
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
|
||||
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
|
||||
// add banned users, so we don't try to invite them
|
||||
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
|
||||
}
|
||||
|
||||
return InviteDialog.buildRecents(alreadyInvited);
|
||||
}
|
||||
|
||||
private onSubmit = async (ev: FormEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
const targets = [...this.state.emailTargets, ...this.state.userTargets];
|
||||
const result = await inviteMultipleToRoom(this.props.roomId, targets);
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
const success = showAnyInviteErrors(result.states, room, result.inviter);
|
||||
if (success) {
|
||||
this.props.onFinished(true);
|
||||
} else {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState({ busy: false });
|
||||
logger.error(e);
|
||||
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
|
||||
title: _t("Failed to invite"),
|
||||
description: ((e && e.message) ? e.message : _t("Operation failed")),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAddressChange = (ev: ChangeEvent<HTMLInputElement>, index: number) => {
|
||||
const targets = arrayFastClone(this.state.emailTargets);
|
||||
if (index >= targets.length) {
|
||||
targets.push(ev.target.value);
|
||||
} else {
|
||||
targets[index] = ev.target.value;
|
||||
}
|
||||
this.setState({ emailTargets: targets });
|
||||
};
|
||||
|
||||
private onAddressBlur = (index: number) => {
|
||||
const targets = arrayFastClone(this.state.emailTargets);
|
||||
if (index >= targets.length) return; // not important
|
||||
if (targets[index].trim() === "") {
|
||||
targets.splice(index, 1);
|
||||
this.setState({ emailTargets: targets });
|
||||
}
|
||||
};
|
||||
|
||||
private onShowPeopleClick = () => {
|
||||
this.setState({ showPeople: !this.state.showPeople });
|
||||
};
|
||||
|
||||
private setPersonToggle = (person: IPerson, selected: boolean) => {
|
||||
const targets = arrayFastClone(this.state.userTargets);
|
||||
if (selected && !targets.includes(person.userId)) {
|
||||
targets.push(person.userId);
|
||||
} else if (!selected && targets.includes(person.userId)) {
|
||||
targets.splice(targets.indexOf(person.userId), 1);
|
||||
}
|
||||
this.setState({ userTargets: targets });
|
||||
};
|
||||
|
||||
private renderPerson(person: IPerson, key: any) {
|
||||
const avatarSize = 36;
|
||||
let avatarUrl = null;
|
||||
if (person.user.getMxcAvatarUrl()) {
|
||||
avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize);
|
||||
}
|
||||
return (
|
||||
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
|
||||
<BaseAvatar
|
||||
url={avatarUrl}
|
||||
name={person.user.name}
|
||||
idName={person.user.userId}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
/>
|
||||
<div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
|
||||
<span className="mx_CommunityPrototypeInviteDialog_personName">{ person.user.name }</span>
|
||||
<span className="mx_CommunityPrototypeInviteDialog_personId">{ person.userId }</span>
|
||||
</div>
|
||||
<StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onShowMorePeople = () => {
|
||||
this.setState({ numPeople: this.state.numPeople + 5 }); // arbitrary increase
|
||||
};
|
||||
|
||||
public render() {
|
||||
const emailAddresses = [];
|
||||
this.state.emailTargets.forEach((address, i) => {
|
||||
emailAddresses.push((
|
||||
<Field
|
||||
key={i}
|
||||
value={address}
|
||||
onChange={(e) => this.onAddressChange(e, i)}
|
||||
label={_t("Email address")}
|
||||
placeholder={_t("Email address")}
|
||||
onBlur={() => this.onAddressBlur(i)}
|
||||
/>
|
||||
));
|
||||
});
|
||||
|
||||
// Push a clean input
|
||||
emailAddresses.push((
|
||||
<Field
|
||||
key={emailAddresses.length}
|
||||
value=""
|
||||
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
|
||||
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
||||
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
||||
/>
|
||||
));
|
||||
|
||||
let peopleIntro = null;
|
||||
const people = [];
|
||||
if (this.state.showPeople) {
|
||||
const humansToPresent = this.state.people.slice(0, this.state.numPeople);
|
||||
humansToPresent.forEach((person, i) => {
|
||||
people.push(this.renderPerson(person, i));
|
||||
});
|
||||
if (humansToPresent.length < this.state.people.length) {
|
||||
people.push((
|
||||
<AccessibleButton
|
||||
onClick={this.onShowMorePeople}
|
||||
kind="link"
|
||||
key="more"
|
||||
className="mx_CommunityPrototypeInviteDialog_morePeople"
|
||||
>
|
||||
{ _t("Show more") }
|
||||
</AccessibleButton>
|
||||
));
|
||||
}
|
||||
}
|
||||
if (this.state.people.length > 0) {
|
||||
peopleIntro = (
|
||||
<div className="mx_CommunityPrototypeInviteDialog_people">
|
||||
<span>{ _t("People you know on %(brand)s", { brand: SdkConfig.get().brand }) }</span>
|
||||
<AccessibleButton onClick={this.onShowPeopleClick}>
|
||||
{ this.state.showPeople ? _t("Hide") : _t("Show") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let buttonText = _t("Skip");
|
||||
const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
|
||||
if (targetCount > 0) {
|
||||
buttonText = _t("Send %(count)s invites", { count: targetCount });
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_CommunityPrototypeInviteDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Invite people to join %(communityName)s", { communityName: this.props.communityName })}
|
||||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
{ emailAddresses }
|
||||
{ peopleIntro }
|
||||
{ people }
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}
|
||||
className="mx_CommunityPrototypeInviteDialog_primaryButton"
|
||||
>
|
||||
{ buttonText }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
|
|||
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
|
||||
|
||||
type BaseProps = ComponentProps<typeof ConfirmUserActionDialog>;
|
||||
interface IProps extends Omit<BaseProps, "groupMember" | "matrixClient" | "children" | "onFinished"> {
|
||||
interface IProps extends Omit<BaseProps, "matrixClient" | "children" | "onFinished"> {
|
||||
space: Room;
|
||||
allLabel: string;
|
||||
specificLabel: string;
|
||||
|
|
|
@ -15,28 +15,20 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { ChangeEvent, FormEvent, ReactNode } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import Field from '../elements/Field';
|
||||
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
||||
|
||||
interface IProps {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member?: RoomMember;
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember?: GroupMemberType;
|
||||
// needed if a group member is specified
|
||||
matrixClient?: MatrixClient;
|
||||
// matrix-js-sdk (room) member object.
|
||||
member: RoomMember;
|
||||
action: string; // eg. 'Ban'
|
||||
title: string; // eg. 'Ban this user?'
|
||||
|
||||
|
@ -112,21 +104,9 @@ export default class ConfirmUserActionDialog extends React.Component<IProps, ISt
|
|||
);
|
||||
}
|
||||
|
||||
let avatar;
|
||||
let name;
|
||||
let userId;
|
||||
if (this.props.member) {
|
||||
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||
name = this.props.member.name;
|
||||
userId = this.props.member.userId;
|
||||
} else {
|
||||
const httpAvatarUrl = this.props.groupMember.avatarUrl
|
||||
? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48)
|
||||
: null;
|
||||
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
||||
userId = this.props.groupMember.userId;
|
||||
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
||||
}
|
||||
const avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||
const name = this.props.member.name;
|
||||
const userId = this.props.member.userId;
|
||||
|
||||
const displayUserIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(
|
||||
userId, { roomId: this.props.roomId, withDisplayName: true },
|
||||
|
|
|
@ -1,236 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import InfoTooltip from "../elements/InfoTooltip";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { showCommunityRoomInviteDialog } from "../../../RoomInvite";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
name: string;
|
||||
localpart: string;
|
||||
error: string;
|
||||
busy: boolean;
|
||||
avatarFile: File;
|
||||
avatarPreview: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateCommunityPrototypeDialog")
|
||||
export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
|
||||
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
name: "",
|
||||
localpart: "",
|
||||
error: null,
|
||||
busy: false,
|
||||
avatarFile: null,
|
||||
avatarPreview: null,
|
||||
};
|
||||
}
|
||||
|
||||
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
|
||||
this.setState({ name: ev.target.value, localpart });
|
||||
};
|
||||
|
||||
private onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.state.busy) return;
|
||||
|
||||
// We'll create the community now to see if it's taken, leaving it active in
|
||||
// the background for the user to look at while they invite people.
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
let avatarUrl = ''; // must be a string for synapse to accept it
|
||||
if (this.state.avatarFile) {
|
||||
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
|
||||
}
|
||||
|
||||
const result = await MatrixClientPeg.get().createGroup({
|
||||
localpart: this.state.localpart,
|
||||
profile: {
|
||||
name: this.state.name,
|
||||
avatar_url: avatarUrl,
|
||||
},
|
||||
});
|
||||
|
||||
// Ensure the tag gets selected now that we've created it
|
||||
dis.dispatch({ action: 'deselect_tags' }, true);
|
||||
dis.dispatch({
|
||||
action: 'select_tag',
|
||||
tag: result.group_id,
|
||||
});
|
||||
|
||||
// Close our own dialog before moving much further
|
||||
this.props.onFinished(true);
|
||||
|
||||
if (result.room_id) {
|
||||
// Force the group store to update as it might have missed the general chat
|
||||
await GroupStore.refreshGroupRooms(result.group_id);
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: result.room_id,
|
||||
metricsTrigger: undefined, // Deprecated groups
|
||||
});
|
||||
showCommunityRoomInviteDialog(result.room_id, this.state.name);
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: result.group_id,
|
||||
group_is_new: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
this.setState({
|
||||
busy: false,
|
||||
error: _t(
|
||||
"There was an error creating your community. The name may be taken or the " +
|
||||
"server is unable to process your request.",
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || !e.target.files.length) {
|
||||
this.setState({ avatarFile: null });
|
||||
} else {
|
||||
this.setState({ busy: true });
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev: ProgressEvent<FileReader>) => {
|
||||
this.setState({ avatarFile: file, busy: false, avatarPreview: ev.target.result as string });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
private onChangeAvatar = () => {
|
||||
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
|
||||
};
|
||||
|
||||
public render() {
|
||||
let communityId = null;
|
||||
if (this.state.localpart) {
|
||||
communityId = (
|
||||
<span className="mx_CreateCommunityPrototypeDialog_communityId">
|
||||
{ _t("Community ID: +<localpart />:%(domain)s", {
|
||||
domain: MatrixClientPeg.getHomeserverName(),
|
||||
}, {
|
||||
localpart: () => <u>{ this.state.localpart }</u>,
|
||||
}) }
|
||||
<InfoTooltip
|
||||
tooltip={_t(
|
||||
"Use this when referencing your community to others. The community ID " +
|
||||
"cannot be changed.",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let helpText = (
|
||||
<span className="mx_CreateCommunityPrototypeDialog_subtext">
|
||||
{ _t("You can change this later if needed.") }
|
||||
</span>
|
||||
);
|
||||
if (this.state.error) {
|
||||
const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error";
|
||||
helpText = (
|
||||
<span className={classes}>
|
||||
{ this.state.error }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
|
||||
if (!this.state.avatarPreview) {
|
||||
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_CreateCommunityPrototypeDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("What's the name of your community or team?")}
|
||||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateCommunityPrototypeDialog_colName">
|
||||
<Field
|
||||
value={this.state.name}
|
||||
onChange={this.onNameChange}
|
||||
placeholder={_t("Enter name")}
|
||||
label={_t("Enter name")}
|
||||
/>
|
||||
{ helpText }
|
||||
<span className="mx_CreateCommunityPrototypeDialog_subtext">
|
||||
{ /*nbsp is to reserve the height of this element when there's nothing*/ }
|
||||
{ communityId }
|
||||
</span>
|
||||
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
|
||||
{ _t("Create") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
|
||||
<input
|
||||
type="file"
|
||||
style={{ display: "none" }}
|
||||
ref={this.avatarUploadRef}
|
||||
accept="image/*"
|
||||
onChange={this.onAvatarChanged}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onChangeAvatar}
|
||||
className="mx_CreateCommunityPrototypeDialog_avatarContainer"
|
||||
>
|
||||
{ preview }
|
||||
</AccessibleButton>
|
||||
<div className="mx_CreateCommunityPrototypeDialog_tip">
|
||||
<b>{ _t("Add image (optional)") }</b>
|
||||
<span>
|
||||
{ _t("An image will help people identify your community.") }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,184 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
groupName: string;
|
||||
groupId: string;
|
||||
groupIdError: string;
|
||||
creating: boolean;
|
||||
createError: Error;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateGroupDialog")
|
||||
export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||
public state = {
|
||||
groupName: '',
|
||||
groupId: '',
|
||||
groupIdError: '',
|
||||
creating: false,
|
||||
createError: null,
|
||||
};
|
||||
|
||||
private onGroupNameChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
groupName: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onGroupIdChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
groupId: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onGroupIdBlur = (): void => {
|
||||
this.checkGroupId();
|
||||
};
|
||||
|
||||
private checkGroupId() {
|
||||
let error = null;
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
} else if (!/^[a-z0-9=_\-./]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
// Reset createError to get rid of now stale error message
|
||||
createError: null,
|
||||
});
|
||||
return error;
|
||||
}
|
||||
|
||||
private onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.checkGroupId()) return;
|
||||
|
||||
const profile: any = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
this.setState({ creating: true });
|
||||
MatrixClientPeg.get().createGroup({
|
||||
localpart: this.state.groupId,
|
||||
profile: profile,
|
||||
}).then((result) => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: result.group_id,
|
||||
group_is_new: true,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
this.setState({ createError: e });
|
||||
}).finally(() => {
|
||||
this.setState({ creating: false });
|
||||
});
|
||||
};
|
||||
|
||||
private onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.creating) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let createErrorNode;
|
||||
if (this.state.createError) {
|
||||
// XXX: We should catch errcodes and give sensible i18ned messages for them,
|
||||
// rather than displaying what the server gives us, but synapse doesn't give
|
||||
// any yet.
|
||||
createErrorNode = <div className="error" role="alert">
|
||||
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
||||
<div>{ this.state.createError.message }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_CreateGroupDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id="groupname"
|
||||
className="mx_CreateGroupDialog_input"
|
||||
autoFocus={true}
|
||||
size={64}
|
||||
placeholder={_t('Example')}
|
||||
onChange={this.onGroupNameChange}
|
||||
value={this.state.groupName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||
</div>
|
||||
<div className="mx_CreateGroupDialog_input_group">
|
||||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||
<input id="groupid"
|
||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||
size={32}
|
||||
placeholder={_t('example')}
|
||||
onChange={this.onGroupIdChange}
|
||||
onBlur={this.onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span className="mx_CreateGroupDialog_suffix">
|
||||
:{ MatrixClientPeg.get().getDomain() }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
{ this.state.groupIdError }
|
||||
</div>
|
||||
{ createErrorNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -26,7 +26,6 @@ import withValidation, { IFieldState } from '../elements/Validation';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Heading from "../typography/Heading";
|
||||
import Field from "../elements/Field";
|
||||
|
@ -122,10 +121,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
createOpts.creation_content = { 'm.federate': false };
|
||||
}
|
||||
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
}
|
||||
|
||||
opts.parentSpace = this.props.parentSpace;
|
||||
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
|
||||
opts.joinRule = JoinRule.Restricted;
|
||||
|
@ -250,14 +245,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
let publicPrivateLabel: JSX.Element;
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
publicPrivateLabel = <p>
|
||||
{ _t(
|
||||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
||||
"found and joined by anyone in this community.",
|
||||
) }
|
||||
</p>;
|
||||
} else if (this.state.joinRule === JoinRule.Restricted) {
|
||||
if (this.state.joinRule === JoinRule.Restricted) {
|
||||
publicPrivateLabel = <p>
|
||||
{ _t(
|
||||
"Everyone in <SpaceName/> will be able to find and join this room.", {}, {
|
||||
|
@ -332,10 +320,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
let title = _t("Create a room");
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
title = _t("Create a room in %(communityName)s", { communityName: name });
|
||||
} else if (!this.props.parentSpace) {
|
||||
if (!this.props.parentSpace) {
|
||||
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,353 +0,0 @@
|
|||
/*
|
||||
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, { useEffect, useRef, useState } from "react";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||
import Field from "../elements/Field";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import { GroupMember } from "../right_panel/UserInfo";
|
||||
import { parseMembersResponse, parseRoomsResponse } from "../../../stores/GroupStore";
|
||||
import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
import { inviteUsersToRoom } from "../../../RoomInvite";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { CreateEventField, IGroupRoom, IGroupSummary } from "../../../@types/groups";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
groupId: string;
|
||||
onFinished(spaceId?: string): void;
|
||||
}
|
||||
|
||||
enum Progress {
|
||||
NotStarted,
|
||||
ValidatingInputs,
|
||||
FetchingData,
|
||||
CreatingSpace,
|
||||
InvitingUsers,
|
||||
// anything beyond here is inviting user n - 4
|
||||
}
|
||||
|
||||
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>(null);
|
||||
|
||||
const [progress, setProgress] = useState(Progress.NotStarted);
|
||||
const [numInvites, setNumInvites] = useState(0);
|
||||
const busy = progress > 0;
|
||||
|
||||
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const [name, setName] = useState("");
|
||||
const spaceNameField = useRef<Field>();
|
||||
const [alias, setAlias] = useState("#" + groupId.substring(1, groupId.indexOf(":")) + ":" + cli.getDomain());
|
||||
const spaceAliasField = useRef<RoomAliasField>();
|
||||
const [topic, setTopic] = useState("");
|
||||
const [joinRule, setJoinRule] = useState<JoinRule>(JoinRule.Public);
|
||||
|
||||
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [groupId]);
|
||||
useEffect(() => {
|
||||
if (groupSummary) {
|
||||
setName(groupSummary.profile.name || "");
|
||||
setTopic(groupSummary.profile.short_description || "");
|
||||
setJoinRule(groupSummary.profile.is_openly_joinable ? JoinRule.Public : JoinRule.Invite);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [groupSummary]);
|
||||
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const onCreateSpaceClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
|
||||
setError(null);
|
||||
setProgress(Progress.ValidatingInputs);
|
||||
|
||||
// require & validate the space name field
|
||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||
setProgress(0);
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||
setProgress(0);
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setProgress(Progress.FetchingData);
|
||||
|
||||
const [rooms, members, invitedMembers] = await Promise.all([
|
||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
]);
|
||||
|
||||
setNumInvites(members.length + invitedMembers.length);
|
||||
|
||||
const viaMap = new Map<string, string[]>();
|
||||
for (const { roomId, canonicalAlias } of rooms) {
|
||||
const room = cli.getRoom(roomId);
|
||||
if (room) {
|
||||
viaMap.set(roomId, calculateRoomVia(room));
|
||||
} else if (canonicalAlias) {
|
||||
try {
|
||||
const { servers } = await cli.getRoomIdForAlias(canonicalAlias);
|
||||
viaMap.set(roomId, servers);
|
||||
} catch (e) {
|
||||
logger.warn("Failed to resolve alias during community migration", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!viaMap.get(roomId)?.length) {
|
||||
// XXX: lets guess the via, this might end up being incorrect.
|
||||
const str = canonicalAlias || roomId;
|
||||
viaMap.set(roomId, [str.substring(1, str.indexOf(":"))]);
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(Progress.CreatingSpace);
|
||||
|
||||
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||
creation_content: {
|
||||
[CreateEventField]: groupId,
|
||||
},
|
||||
initial_state: rooms.map(({ roomId }) => ({
|
||||
type: EventType.SpaceChild,
|
||||
state_key: roomId,
|
||||
content: {
|
||||
via: viaMap.get(roomId) || [],
|
||||
},
|
||||
})),
|
||||
// we do not specify the inviters here because Synapse applies a limit and this may cause it to trip
|
||||
}, {
|
||||
andView: false,
|
||||
});
|
||||
|
||||
setProgress(Progress.InvitingUsers);
|
||||
|
||||
const userIds = [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId());
|
||||
await inviteUsersToRoom(roomId, userIds, false, () => setProgress(p => p + 1));
|
||||
|
||||
// eagerly remove it from the community panel
|
||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||
|
||||
// don't bother awaiting this, as we don't hugely care if it fails
|
||||
cli.setGroupProfile(groupId, {
|
||||
...groupSummary.profile,
|
||||
long_description: `<a href="${makeRoomPermalink(roomId)}"><h1>` +
|
||||
_t("This community has been upgraded into a Space") + `</h1></a><br />`
|
||||
+ groupSummary.profile.long_description,
|
||||
} as IGroupSummary["profile"]).catch(e => {
|
||||
logger.warn("Failed to update community profile during migration", e);
|
||||
});
|
||||
|
||||
onFinished(roomId);
|
||||
|
||||
const onSpaceClick = () => {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined, // other
|
||||
});
|
||||
};
|
||||
|
||||
const onPreferencesClick = () => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Preferences,
|
||||
});
|
||||
};
|
||||
|
||||
let spacesDisabledCopy;
|
||||
if (!SpaceStore.spacesEnabled) {
|
||||
spacesDisabledCopy = _t("To view Spaces, hide communities in <a>Preferences</a>", {}, {
|
||||
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("Space created"),
|
||||
description: <>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark" />
|
||||
<p>
|
||||
{ _t("<SpaceName/> has been made and everyone who was a part of the community has " +
|
||||
"been invited to it.", {}, {
|
||||
SpaceName: () => <AccessibleButton onClick={onSpaceClick} kind="link">
|
||||
{ name }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
|
||||
{ spacesDisabledCopy }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("To create a Space from another community, just pick the community in Preferences.") }
|
||||
</p>
|
||||
</>,
|
||||
button: _t("Preferences"),
|
||||
onFinished: (openPreferences: boolean) => {
|
||||
if (openPreferences) {
|
||||
onPreferencesClick();
|
||||
}
|
||||
},
|
||||
}, "mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog");
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
setError(e);
|
||||
}
|
||||
|
||||
setProgress(Progress.NotStarted);
|
||||
};
|
||||
|
||||
let footer;
|
||||
if (error) {
|
||||
footer = <>
|
||||
<img src={require("../../../../res/img/element-icons/warning-badge.svg").default} height="24" width="24" alt="" />
|
||||
|
||||
<span className="mx_CreateSpaceFromCommunityDialog_error">
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_errorHeading">{ _t("Failed to migrate community") }</div>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_errorCaption">{ _t("Try again") }</div>
|
||||
</span>
|
||||
|
||||
<AccessibleButton className="mx_CreateSpaceFromCommunityDialog_retryButton" onClick={onCreateSpaceClick}>
|
||||
{ _t("Retry") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
} else if (busy) {
|
||||
let description: string;
|
||||
switch (progress) {
|
||||
case Progress.ValidatingInputs:
|
||||
case Progress.FetchingData:
|
||||
description = _t("Fetching data...");
|
||||
break;
|
||||
case Progress.CreatingSpace:
|
||||
description = _t("Creating Space...");
|
||||
break;
|
||||
case Progress.InvitingUsers:
|
||||
default:
|
||||
description = _t("Adding rooms... (%(progress)s out of %(count)s)", {
|
||||
count: numInvites,
|
||||
progress,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
footer = <span>
|
||||
<ProgressBar
|
||||
value={progress > Progress.FetchingData ? progress : 0}
|
||||
max={numInvites + Progress.InvitingUsers}
|
||||
/>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_progressText">
|
||||
{ description }
|
||||
</div>
|
||||
</span>;
|
||||
} else {
|
||||
footer = <>
|
||||
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
|
||||
{ _t("Create Space") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Create Space from community")}
|
||||
className="mx_CreateSpaceFromCommunityDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_content">
|
||||
<p>
|
||||
{ _t("A link to the Space will be put in your community description.") }
|
||||
|
||||
{ _t("All rooms will be added and all community members will be invited.") }
|
||||
</p>
|
||||
<p className="mx_CreateSpaceFromCommunityDialog_flairNotice">
|
||||
{ _t("Flair won't be available in Spaces for the foreseeable future.") }
|
||||
</p>
|
||||
|
||||
<SpaceCreateForm
|
||||
busy={busy}
|
||||
onSubmit={onCreateSpaceClick}
|
||||
avatarUrl={groupSummary.profile.avatar_url
|
||||
? mediaFromMxc(groupSummary.profile.avatar_url).getThumbnailOfSourceHttp(80, 80, "crop")
|
||||
: undefined
|
||||
}
|
||||
setAvatar={setAvatar}
|
||||
name={name}
|
||||
setName={setName}
|
||||
nameFieldRef={spaceNameField}
|
||||
topic={topic}
|
||||
setTopic={setTopic}
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
showAliasField={joinRule === JoinRule.Public}
|
||||
aliasFieldRef={spaceAliasField}
|
||||
>
|
||||
<p>{ _t("This description will be shown to people when they view your space") }</p>
|
||||
<JoinRuleDropdown
|
||||
label={_t("Space visibility")}
|
||||
labelInvite={_t("Private space (invite only)")}
|
||||
labelPublic={_t("Public space")}
|
||||
value={joinRule}
|
||||
onChange={setJoinRule}
|
||||
/>
|
||||
<p>{ joinRule === JoinRule.Public
|
||||
? _t("Open space for anyone, best for communities")
|
||||
: _t("Invite only, best for yourself or teams")
|
||||
}</p>
|
||||
{ joinRule !== JoinRule.Public &&
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_nonPublicSpacer" />
|
||||
}
|
||||
</SpaceCreateForm>
|
||||
</div>
|
||||
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default CreateSpaceFromCommunityDialog;
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
communityId: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
name: string;
|
||||
error: string;
|
||||
busy: boolean;
|
||||
currentAvatarUrl: string;
|
||||
avatarFile: File;
|
||||
avatarPreview: string;
|
||||
}
|
||||
|
||||
// XXX: This is a lot of duplication from the create dialog, just in a different shape
|
||||
@replaceableComponent("views.dialogs.EditCommunityPrototypeDialog")
|
||||
export default class EditCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
|
||||
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const profile = CommunityPrototypeStore.instance.getCommunityProfile(props.communityId);
|
||||
|
||||
this.state = {
|
||||
name: profile?.name || "",
|
||||
error: null,
|
||||
busy: false,
|
||||
avatarFile: null,
|
||||
avatarPreview: null,
|
||||
currentAvatarUrl: profile?.avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ name: ev.target.value });
|
||||
};
|
||||
|
||||
private onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.state.busy) return;
|
||||
|
||||
// We'll create the community now to see if it's taken, leaving it active in
|
||||
// the background for the user to look at while they invite people.
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
let avatarUrl = this.state.currentAvatarUrl || ""; // must be a string for synapse to accept it
|
||||
if (this.state.avatarFile) {
|
||||
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
|
||||
}
|
||||
|
||||
await MatrixClientPeg.get().setGroupProfile(this.props.communityId, {
|
||||
name: this.state.name,
|
||||
avatar_url: avatarUrl,
|
||||
});
|
||||
|
||||
// ask the flair store to update the profile too
|
||||
await FlairStore.refreshGroupProfile(MatrixClientPeg.get(), this.props.communityId);
|
||||
|
||||
// we did it, so close the dialog
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
this.setState({
|
||||
busy: false,
|
||||
error: _t("There was an error updating your community. The server is unable to process your request."),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || !e.target.files.length) {
|
||||
this.setState({ avatarFile: null });
|
||||
} else {
|
||||
this.setState({ busy: true });
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev: ProgressEvent<FileReader>) => {
|
||||
this.setState({ avatarFile: file, busy: false, avatarPreview: ev.target.result as string });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
private onChangeAvatar = () => {
|
||||
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
|
||||
};
|
||||
|
||||
public render() {
|
||||
let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
|
||||
if (!this.state.avatarPreview) {
|
||||
if (this.state.currentAvatarUrl) {
|
||||
const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
|
||||
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
|
||||
} else {
|
||||
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_EditCommunityPrototypeDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Update community")}
|
||||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_EditCommunityPrototypeDialog_rowName">
|
||||
<Field
|
||||
value={this.state.name}
|
||||
onChange={this.onNameChange}
|
||||
placeholder={_t("Enter name")}
|
||||
label={_t("Enter name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_EditCommunityPrototypeDialog_rowAvatar">
|
||||
<input
|
||||
type="file"
|
||||
style={{ display: "none" }}
|
||||
ref={this.avatarUploadRef}
|
||||
accept="image/*"
|
||||
onChange={this.onAvatarChanged}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onChangeAvatar}
|
||||
className="mx_EditCommunityPrototypeDialog_avatarContainer"
|
||||
>{ preview }</AccessibleButton>
|
||||
<div className="mx_EditCommunityPrototypeDialog_tip">
|
||||
<b>{ _t("Add image (optional)") }</b>
|
||||
<span>
|
||||
{ _t("An image will help people identify your community.") }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,8 +23,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { Layout } from "../../../settings/enums/Layout";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
|
@ -43,7 +42,6 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
|||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { roomContextDetailsText } from "../../../Rooms";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
@ -190,16 +188,13 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const spacesEnabled = SpaceStore.spacesEnabled;
|
||||
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
|
||||
const previewLayout = useSettingValue<Layout>("layout");
|
||||
|
||||
let rooms = useMemo(() => sortRooms(
|
||||
cli.getVisibleRooms().filter(
|
||||
room => room.getMyMembership() === "join" &&
|
||||
!(spacesEnabled && room.isSpaceRoom()),
|
||||
room => room.getMyMembership() === "join" && !room.isSpaceRoom(),
|
||||
),
|
||||
), [cli, spacesEnabled]);
|
||||
), [cli]);
|
||||
|
||||
if (lcQuery) {
|
||||
rooms = new QueryMatcher<Room>(rooms, {
|
||||
|
@ -241,7 +236,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
|||
<EventTile
|
||||
mxEvent={mockEvent}
|
||||
layout={previewLayout}
|
||||
enableFlair={flairEnabled}
|
||||
permalinkCreator={permalinkCreator}
|
||||
as="div"
|
||||
/>
|
||||
|
|
|
@ -43,12 +43,10 @@ import {
|
|||
IInviteResult,
|
||||
inviteMultipleToRoom,
|
||||
showAnyInviteErrors,
|
||||
showCommunityInviteDialog,
|
||||
} from "../../../RoomInvite";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
@ -64,7 +62,6 @@ import QuestionDialog from "./QuestionDialog";
|
|||
import Spinner from "../elements/Spinner";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
|
@ -1104,23 +1101,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onCommunityInviteClick = (e) => {
|
||||
this.props.onFinished(false);
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
};
|
||||
|
||||
private renderSection(kind: "recents"|"suggestions") {
|
||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||
const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this);
|
||||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||
let sectionSubname = null;
|
||||
|
||||
if (kind === 'suggestions' && CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
sectionSubname = _t("May include members not in %(communityName)s", { communityName });
|
||||
}
|
||||
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
||||
|
@ -1199,7 +1185,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{ sectionName }</h3>
|
||||
{ sectionSubname ? <p className="mx_InviteDialog_subname">{ sectionSubname }</p> : null }
|
||||
{ tiles }
|
||||
{ showMore }
|
||||
</div>
|
||||
|
@ -1247,7 +1232,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||
if (defaultIdentityServerUrl) {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
<div className="mx_InviteDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
|
@ -1268,7 +1253,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
<div className="mx_InviteDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
|
@ -1377,35 +1362,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
);
|
||||
}
|
||||
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
const inviteText = _t(
|
||||
"This won't invite them to %(communityName)s. " +
|
||||
"To invite someone to %(communityName)s, click <a>here</a>",
|
||||
{ communityName }, {
|
||||
userId: () => {
|
||||
return (
|
||||
<a
|
||||
href={makeUserPermalink(userId)}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>{ userId }</a>
|
||||
);
|
||||
},
|
||||
a: (sub) => {
|
||||
return (
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={this.onCommunityInviteClick}
|
||||
>{ sub }</AccessibleButton>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
helpText = <React.Fragment>
|
||||
{ helpText } { inviteText }
|
||||
</React.Fragment>;
|
||||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this.startDm;
|
||||
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
||||
|
@ -1423,7 +1379,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
</div>;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = SpaceStore.spacesEnabled && room?.isSpaceRoom();
|
||||
const isSpace = room?.isSpaceRoom();
|
||||
title = isSpace
|
||||
? _t("Invite to %(spaceName)s", {
|
||||
spaceName: room.name || _t("Unnamed Space"),
|
||||
|
|
|
@ -18,13 +18,12 @@ limitations under the License.
|
|||
import * as React from 'react';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { Group } from "matrix-js-sdk/src/models/group";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from "../elements/QRCode";
|
||||
import { RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import { RoomPermalinkCreator, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import { selectText } from "../../../utils/strings";
|
||||
import StyledCheckbox from '../elements/StyledCheckbox';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
|
@ -63,7 +62,7 @@ const socials = [
|
|||
];
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
target: Room | User | Group | RoomMember | MatrixEvent;
|
||||
target: Room | User | RoomMember | MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
|
@ -121,8 +120,6 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
} else if (this.props.target instanceof Group) {
|
||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
|
||||
|
@ -153,8 +150,6 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
} else if (this.props.target instanceof Group) {
|
||||
title = _t('Share Community');
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
title = _t('Share Room Message');
|
||||
checkbox = <div>
|
||||
|
|
|
@ -28,7 +28,6 @@ import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserS
|
|||
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
|
||||
import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
|
||||
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
|
||||
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
|
@ -41,7 +40,6 @@ import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsT
|
|||
export enum UserTab {
|
||||
General = "USER_GENERAL_TAB",
|
||||
Appearance = "USER_APPEARANCE_TAB",
|
||||
Flair = "USER_FLAIR_TAB",
|
||||
Notifications = "USER_NOTIFICATIONS_TAB",
|
||||
Preferences = "USER_PREFERENCES_TAB",
|
||||
Keyboard = "USER_KEYBOARD_TAB",
|
||||
|
@ -103,15 +101,6 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
|||
<AppearanceUserSettingsTab />,
|
||||
"UserSettingsAppearance",
|
||||
));
|
||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||
tabs.push(new Tab(
|
||||
UserTab.Flair,
|
||||
_td("Flair"),
|
||||
"mx_UserSettingsDialog_flairIcon",
|
||||
<FlairUserSettingsTab />,
|
||||
"UserSettingFlair",
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
UserTab.Notifications,
|
||||
_td("Notifications"),
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
/* eslint new-cap: "off" */
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 TagTile from './TagTile';
|
||||
import ContextMenu, { toRightOf, useContextMenu } from "../../structures/ContextMenu";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
export default function DNDTagTile(props) {
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
let contextMenu = null;
|
||||
if (menuDisplayed && handle.current) {
|
||||
const elementRect = handle.current.getBoundingClientRect();
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
|
||||
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} index={props.index} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
return <>
|
||||
<TagTile
|
||||
{...props}
|
||||
contextMenuButtonRef={handle}
|
||||
menuDisplayed={menuDisplayed}
|
||||
openMenu={openMenu}
|
||||
/>
|
||||
{ contextMenu }
|
||||
</>;
|
||||
}
|
|
@ -98,7 +98,7 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
|
|||
{ _t(
|
||||
"Debug logs contain application " +
|
||||
"usage data including your username, the IDs or aliases of " +
|
||||
"the rooms or groups you have visited, which UI elements you " +
|
||||
"the rooms you have visited, which UI elements you " +
|
||||
"last interacted with, and the usernames of other users. " +
|
||||
"They do not contain messages.",
|
||||
) }</p>
|
||||
|
|
|
@ -21,9 +21,7 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
|
|||
|
||||
import * as Avatar from '../../../Avatar';
|
||||
import EventTile from '../rooms/EventTile';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { Layout } from "../../../settings/enums/Layout";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Spinner from './Spinner';
|
||||
|
||||
|
@ -133,7 +131,6 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
|
|||
<EventTile
|
||||
mxEvent={event}
|
||||
layout={this.props.layout}
|
||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||
as="div"
|
||||
/>
|
||||
</div>;
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
class FlairAvatar extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
ev.preventDefault();
|
||||
// Don't trigger onClick of parent element
|
||||
ev.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupProfile.groupId,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16);
|
||||
const tooltip = this.props.groupProfile.name ?
|
||||
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
|
||||
this.props.groupProfile.groupId;
|
||||
return <img
|
||||
src={httpUrl}
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={this.onClick}
|
||||
title={tooltip} />;
|
||||
}
|
||||
}
|
||||
|
||||
FlairAvatar.propTypes = {
|
||||
groupProfile: PropTypes.shape({
|
||||
groupId: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
avatarUrl: PropTypes.string.isRequired,
|
||||
}),
|
||||
};
|
||||
|
||||
FlairAvatar.contextType = MatrixClientContext;
|
||||
|
||||
@replaceableComponent("views.elements.Flair")
|
||||
export default class Flair extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
profiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
this._generateAvatars(this.props.groups);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
||||
this._generateAvatars(newProps.groups);
|
||||
}
|
||||
|
||||
async _getGroupProfiles(groups) {
|
||||
const profiles = [];
|
||||
for (const groupId of groups) {
|
||||
let groupProfile = null;
|
||||
try {
|
||||
groupProfile = await FlairStore.getGroupProfileCached(this.context, groupId);
|
||||
} catch (err) {
|
||||
logger.error('Could not get profile for group', groupId, err);
|
||||
}
|
||||
profiles.push(groupProfile);
|
||||
}
|
||||
return profiles.filter((p) => p !== null);
|
||||
}
|
||||
|
||||
async _generateAvatars(groups) {
|
||||
if (!groups || groups.length === 0) {
|
||||
return;
|
||||
}
|
||||
const profiles = await this._getGroupProfiles(groups);
|
||||
if (!this.unmounted) {
|
||||
this.setState({
|
||||
profiles: profiles.filter((profile) => {
|
||||
return profile ? profile.avatarUrl : false;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.profiles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const avatars = this.state.profiles.map((profile, index) => {
|
||||
return <FlairAvatar key={index} groupProfile={profile} />;
|
||||
});
|
||||
return (
|
||||
<span className="mx_Flair">
|
||||
{ avatars }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Flair.propTypes = {
|
||||
groups: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
Flair.contextType = MatrixClientContext;
|
|
@ -23,11 +23,9 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import { getPrimaryPermalinkEntity, parsePermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import Tooltip from './Tooltip';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
|
@ -43,7 +41,6 @@ class Pill extends React.Component {
|
|||
|
||||
static TYPE_USER_MENTION = 'TYPE_USER_MENTION';
|
||||
static TYPE_ROOM_MENTION = 'TYPE_ROOM_MENTION';
|
||||
static TYPE_GROUP_MENTION = 'TYPE_GROUP_MENTION';
|
||||
static TYPE_AT_ROOM_MENTION = 'TYPE_AT_ROOM_MENTION'; // '@room' mention
|
||||
|
||||
static propTypes = {
|
||||
|
@ -69,8 +66,6 @@ class Pill extends React.Component {
|
|||
|
||||
// The member related to the user pill
|
||||
member: null,
|
||||
// The group related to the group pill
|
||||
group: null,
|
||||
// The room related to the room pill
|
||||
room: null,
|
||||
// Is the user hovering the pill
|
||||
|
@ -98,11 +93,9 @@ class Pill extends React.Component {
|
|||
'@': Pill.TYPE_USER_MENTION,
|
||||
'#': Pill.TYPE_ROOM_MENTION,
|
||||
'!': Pill.TYPE_ROOM_MENTION,
|
||||
'+': Pill.TYPE_GROUP_MENTION,
|
||||
}[prefix];
|
||||
|
||||
let member;
|
||||
let group;
|
||||
let room;
|
||||
switch (pillType) {
|
||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||
|
@ -116,8 +109,8 @@ class Pill extends React.Component {
|
|||
member = new RoomMember(null, resourceId);
|
||||
this.doProfileLookup(resourceId, member);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Pill.TYPE_ROOM_MENTION: {
|
||||
const localRoom = resourceId[0] === '#' ?
|
||||
MatrixClientPeg.get().getRooms().find((r) => {
|
||||
|
@ -130,23 +123,10 @@ class Pill extends React.Component {
|
|||
// a room avatar and name.
|
||||
// this.doRoomProfileLookup(resourceId, member);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
try {
|
||||
group = await FlairStore.getGroupProfileCached(cli, resourceId);
|
||||
} catch (e) { // if FlairStore failed, fall back to just groupId
|
||||
group = {
|
||||
groupId: resourceId,
|
||||
avatarUrl: null,
|
||||
name: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({ resourceId, pillType, member, group, room });
|
||||
this.setState({ resourceId, pillType, member, room });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -203,7 +183,6 @@ class Pill extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
|
@ -225,8 +204,8 @@ class Pill extends React.Component {
|
|||
}
|
||||
pillClass = 'mx_AtRoomPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Pill.TYPE_USER_MENTION: {
|
||||
// If this user is not a member of this room, default to the empty member
|
||||
const member = this.state.member;
|
||||
|
@ -241,8 +220,8 @@ class Pill extends React.Component {
|
|||
href = null;
|
||||
onClick = this.onUserPillClicked;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Pill.TYPE_ROOM_MENTION: {
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
|
@ -252,25 +231,8 @@ class Pill extends React.Component {
|
|||
}
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
if (this.state.group) {
|
||||
const { avatarUrl, groupId, name } = this.state.group;
|
||||
|
||||
linkText = groupId;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <BaseAvatar
|
||||
name={name || groupId}
|
||||
width={16}
|
||||
height={16}
|
||||
aria-hidden="true"
|
||||
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
|
||||
}
|
||||
pillClass = 'mx_GroupPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const classes = classNames("mx_Pill", pillClass, {
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
// A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
|
||||
// a thing to click on for the user to filter the visible rooms in the RoomList to:
|
||||
// - Rooms that are part of the group
|
||||
// - Direct messages with members of the group
|
||||
// with the intention that this could be expanded to arbitrary tags in future.
|
||||
@replaceableComponent("views.elements.TagTile")
|
||||
export default class TagTile extends React.Component {
|
||||
static propTypes = {
|
||||
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
|
||||
// For now, only group IDs are handled.
|
||||
tag: PropTypes.string,
|
||||
contextMenuButtonRef: PropTypes.object,
|
||||
openMenu: PropTypes.func,
|
||||
menuDisplayed: PropTypes.bool,
|
||||
selected: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
state = {
|
||||
// Whether the mouse is over the tile
|
||||
hover: false,
|
||||
// The profile data of the group if this.props.tag is a group ID
|
||||
profile: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
|
||||
this._onFlairStoreUpdated();
|
||||
// New rooms or members may have been added to the group, fetch async
|
||||
this._refreshGroup(this.props.tag);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
if (this.props.tag[0] === '+') {
|
||||
FlairStore.removeListener('updateGroupProfile', this._onFlairStoreUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
_onFlairStoreUpdated = () => {
|
||||
if (this.unmounted) return;
|
||||
FlairStore.getGroupProfileCached(
|
||||
this.context,
|
||||
this.props.tag,
|
||||
).then((profile) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({ profile });
|
||||
}).catch((err) => {
|
||||
logger.warn('Could not fetch group profile for ' + this.props.tag, err);
|
||||
});
|
||||
};
|
||||
|
||||
_refreshGroup(groupId) {
|
||||
GroupStore.refreshGroupRooms(groupId);
|
||||
GroupStore.refreshGroupMembers(groupId);
|
||||
}
|
||||
|
||||
onClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch({
|
||||
action: 'select_tag',
|
||||
tag: this.props.tag,
|
||||
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
|
||||
shiftKey: e.shiftKey,
|
||||
});
|
||||
if (this.props.tag[0] === '+') {
|
||||
// New rooms or members may have been added to the group, fetch async
|
||||
this._refreshGroup(this.props.tag);
|
||||
}
|
||||
};
|
||||
|
||||
onMouseOver = () => {
|
||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) return;
|
||||
this.setState({ hover: true });
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({ hover: false });
|
||||
};
|
||||
|
||||
openMenu = e => {
|
||||
// Prevent the TagTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) return;
|
||||
this.setState({ hover: false });
|
||||
this.props.openMenu();
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
const avatarSize = 32;
|
||||
|
||||
const httpUrl = profile.avatarUrl
|
||||
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarSize)
|
||||
: null;
|
||||
|
||||
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
|
||||
const className = classNames({
|
||||
mx_TagTile: true,
|
||||
mx_TagTile_prototype: isPrototype,
|
||||
mx_TagTile_selected: this.props.selected && !isPrototype,
|
||||
mx_TagTile_selected_prototype: this.props.selected && isPrototype,
|
||||
});
|
||||
|
||||
const badge = GroupFilterOrderStore.getGroupBadge(this.props.tag);
|
||||
let badgeElement;
|
||||
if (badge && !this.state.hover && !this.props.menuDisplayed) {
|
||||
const badgeClasses = classNames({
|
||||
"mx_TagTile_badge": true,
|
||||
"mx_TagTile_badgeHighlight": badge.highlight,
|
||||
});
|
||||
badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badge.count) }</div>);
|
||||
}
|
||||
|
||||
const contextButton = this.state.hover || this.props.menuDisplayed ?
|
||||
<AccessibleButton
|
||||
className="mx_TagTile_context_button"
|
||||
onClick={this.openMenu}
|
||||
inputRef={this.props.contextMenuButtonRef}
|
||||
>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />;
|
||||
|
||||
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
className={className}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.openMenu}
|
||||
title={name}
|
||||
>
|
||||
<div
|
||||
className="mx_TagTile_avatar"
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.tag}
|
||||
url={httpUrl}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
/>
|
||||
{ contextButton }
|
||||
{ badgeElement }
|
||||
</div>
|
||||
</AccessibleTooltipButton>;
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import classNames from "classnames";
|
||||
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.elements.UserTagTile")
|
||||
export default class UserTagTile extends React.PureComponent<IProps, IState> {
|
||||
private tagStoreRef: fbEmitter.EventSubscription;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selected: GroupFilterOrderStore.getSelectedTags().length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.tagStoreRef.remove();
|
||||
}
|
||||
|
||||
private onTagStoreUpdate = () => {
|
||||
const selected = GroupFilterOrderStore.getSelectedTags().length === 0;
|
||||
this.setState({ selected });
|
||||
};
|
||||
|
||||
private onTileClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// Deselect all tags
|
||||
defaultDispatcher.dispatch({ action: "deselect_tags" });
|
||||
};
|
||||
|
||||
public render() {
|
||||
// XXX: We reuse TagTile classes for ease of demonstration - we should probably generify
|
||||
// TagTile instead if we continue to use this component.
|
||||
const className = classNames({
|
||||
mx_TagTile: true,
|
||||
mx_TagTile_prototype: true,
|
||||
mx_TagTile_selected_prototype: this.state.selected,
|
||||
mx_TagTile_home: true,
|
||||
});
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className={className}
|
||||
onClick={this.onTileClick}
|
||||
title={_t("Home")}
|
||||
>
|
||||
<div className="mx_TagTile_avatar">
|
||||
<div className="mx_TagTile_homeIcon" />
|
||||
</div>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
/*
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import ContextMenu, { ContextMenuButton, toRightOf } from "../../structures/ContextMenu";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
// XXX this class copies a lot from RoomTile.js
|
||||
@replaceableComponent("views.groups.GroupInviteTile")
|
||||
export default class GroupInviteTile extends React.Component {
|
||||
static propTypes = {
|
||||
group: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
menuDisplayed: false,
|
||||
selected: this.props.group.groupId === null, // XXX: this needs linking to LoggedInView/GroupView state
|
||||
};
|
||||
}
|
||||
|
||||
onClick = e => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.group.groupId,
|
||||
});
|
||||
};
|
||||
|
||||
onMouseEnter = () => {
|
||||
const state = { hover: true };
|
||||
// Only allow non-guests to access the context menu
|
||||
if (!this.context.isGuest()) {
|
||||
state.badgeHover = true;
|
||||
}
|
||||
this.setState(state);
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({
|
||||
badgeHover: false,
|
||||
hover: false,
|
||||
});
|
||||
};
|
||||
|
||||
_showContextMenu(boundingClientRect) {
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const state = {
|
||||
contextMenuPosition: boundingClientRect,
|
||||
};
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
state.hover = false;
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
onContextMenuButtonClick = e => {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
this._showContextMenu(e.target.getBoundingClientRect());
|
||||
};
|
||||
|
||||
onContextMenu = e => {
|
||||
// Prevent the native context menu
|
||||
e.preventDefault();
|
||||
|
||||
this._showContextMenu({
|
||||
right: e.clientX,
|
||||
top: e.clientY,
|
||||
height: 0,
|
||||
});
|
||||
};
|
||||
|
||||
closeMenu = () => {
|
||||
this.setState({
|
||||
contextMenuPosition: null,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
const groupName = this.props.group.name || this.props.group.groupId;
|
||||
const httpAvatarUrl = this.props.group.avatarUrl
|
||||
? mediaFromMxc(this.props.group.avatarUrl).getSquareThumbnailHttp(24)
|
||||
: null;
|
||||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||
|
||||
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
|
||||
const nameClasses = classNames('mx_RoomTile_title mx_RoomTile_invite mx_RoomTile_badgeShown', {
|
||||
'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed,
|
||||
});
|
||||
|
||||
// XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
|
||||
const label = <div title={this.props.group.groupId} className={nameClasses} tabIndex={-1} dir="auto">
|
||||
{ groupName }
|
||||
</div>;
|
||||
|
||||
const badgeEllipsis = this.state.badgeHover || isMenuDisplayed;
|
||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_badgeButton': badgeEllipsis,
|
||||
});
|
||||
|
||||
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
||||
|
||||
let tooltip;
|
||||
if (this.props.collapsed && this.state.hover) {
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
|
||||
}
|
||||
|
||||
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_menuDisplayed': isMenuDisplayed,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_GroupInviteTile': true,
|
||||
});
|
||||
|
||||
let contextMenu;
|
||||
if (isMenuDisplayed) {
|
||||
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
|
||||
contextMenu = (
|
||||
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
|
||||
<GroupInviteTileContextMenu group={this.props.group} onFinished={this.closeMenu} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <React.Fragment>
|
||||
<RovingTabIndexWrapper>
|
||||
{ ({ onFocus, isActive, ref }) =>
|
||||
<AccessibleButton
|
||||
onFocus={onFocus}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
inputRef={ref}
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
<div className="mx_RoomTile_details">
|
||||
<div className="mx_RoomTile_primaryDetails">
|
||||
<div className="mx_RoomTile_titleContainer">
|
||||
{ label }
|
||||
<ContextMenuButton
|
||||
className={badgeClasses}
|
||||
onClick={this.onContextMenuButtonClick}
|
||||
label={_t("Options")}
|
||||
isExpanded={isMenuDisplayed}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
{ badgeContent }
|
||||
</ContextMenuButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
|
@ -1,244 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import { showGroupInviteDialog } from '../../../GroupAddressPicker';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
|
||||
@replaceableComponent("views.groups.GroupMemberList")
|
||||
export default class GroupMemberList extends React.Component {
|
||||
static propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
members: null,
|
||||
membersError: null,
|
||||
invitedMembers: null,
|
||||
invitedMembersError: null,
|
||||
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
this._initGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
GroupStore.registerListener(groupId, () => {
|
||||
this._fetchMembers();
|
||||
});
|
||||
GroupStore.on('error', (err, errorGroupId, stateKey) => {
|
||||
if (this._unmounted || groupId !== errorGroupId) return;
|
||||
if (stateKey === GroupStore.STATE_KEY.GroupMembers) {
|
||||
this.setState({
|
||||
membersError: err,
|
||||
});
|
||||
}
|
||||
if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers) {
|
||||
this.setState({
|
||||
invitedMembersError: err,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_fetchMembers() {
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
members: GroupStore.getGroupMembers(this.props.groupId),
|
||||
invitedMembers: GroupStore.getGroupInvitedMembers(this.props.groupId),
|
||||
});
|
||||
}
|
||||
|
||||
_createOverflowTile = (overflowCount, totalCount) => {
|
||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile
|
||||
className="mx_EntityTile_ellipsis"
|
||||
avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." width={36} height={36} />
|
||||
}
|
||||
name={text}
|
||||
presenceState="online"
|
||||
suppressOnHover={true}
|
||||
onClick={this._showFullMemberList}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_showFullMemberList = () => {
|
||||
this.setState({
|
||||
truncateAt: -1,
|
||||
});
|
||||
};
|
||||
|
||||
onSearchQueryChanged = ev => {
|
||||
this.setState({ searchQuery: ev.target.value });
|
||||
};
|
||||
|
||||
makeGroupMemberTiles(query, memberList, memberListError) {
|
||||
if (memberListError) {
|
||||
return <div className="warning">{ _t("Failed to load group members") }</div>;
|
||||
}
|
||||
|
||||
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
query = (query || "").toLowerCase();
|
||||
if (query) {
|
||||
memberList = memberList.filter((m) => {
|
||||
const matchesName = (m.displayname || "").toLowerCase().includes(query);
|
||||
const matchesId = m.userId.toLowerCase().includes(query);
|
||||
|
||||
if (!matchesName && !matchesId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const uniqueMembers = {};
|
||||
memberList.forEach((m) => {
|
||||
if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
|
||||
});
|
||||
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
|
||||
// Descending sort on isPrivileged = true = 1 to isPrivileged = false = 0
|
||||
memberList.sort((a, b) => {
|
||||
if (a.isPrivileged === b.isPrivileged) {
|
||||
const aName = a.displayname || a.userId;
|
||||
const bName = b.displayname || b.userId;
|
||||
if (aName < bName) {
|
||||
return -1;
|
||||
} else if (aName > bName) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return a.isPrivileged ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
const memberTiles = memberList.map((m) => {
|
||||
return (
|
||||
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} />
|
||||
);
|
||||
});
|
||||
|
||||
return <TruncatedList
|
||||
className="mx_MemberList_wrapper"
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}
|
||||
>
|
||||
{ memberTiles }
|
||||
</TruncatedList>;
|
||||
}
|
||||
|
||||
onInviteToGroupButtonClick = () => {
|
||||
showGroupInviteDialog(this.props.groupId).then(() => {
|
||||
RightPanelStore.instance.setCard({
|
||||
phase: RightPanelPhases.GroupMemberList,
|
||||
state: { groupId: this.props.groupId },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.fetching || this.state.fetchingInvitedMembers) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return (<div className="mx_MemberList">
|
||||
<Spinner />
|
||||
</div>);
|
||||
}
|
||||
|
||||
const inputBox = (
|
||||
<input
|
||||
className="mx_GroupMemberList_query mx_textinput"
|
||||
id="mx_GroupMemberList_query"
|
||||
type="text"
|
||||
onChange={this.onSearchQueryChanged}
|
||||
value={this.state.searchQuery}
|
||||
placeholder={_t('Filter community members')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
||||
const joined = this.state.members ? <div className="mx_MemberList_joined">
|
||||
{
|
||||
this.makeGroupMemberTiles(
|
||||
this.state.searchQuery,
|
||||
this.state.members,
|
||||
this.state.membersError,
|
||||
)
|
||||
}
|
||||
</div> : <div />;
|
||||
|
||||
const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
|
||||
<div className="mx_MemberList_invited">
|
||||
<h2>{ _t("Invited") }</h2>
|
||||
{
|
||||
this.makeGroupMemberTiles(
|
||||
this.state.searchQuery,
|
||||
this.state.invitedMembers,
|
||||
this.state.invitedMembersError,
|
||||
)
|
||||
}
|
||||
</div> : <div />;
|
||||
|
||||
let inviteButton;
|
||||
if (GroupStore.isUserPrivileged(this.props.groupId)) {
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
className="mx_MemberList_invite mx_MemberList_inviteCommunity"
|
||||
onClick={this.onInviteToGroupButtonClick}
|
||||
>
|
||||
<span>{ _t('Invite to this community') }</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MemberList" role="tabpanel">
|
||||
{ inviteButton }
|
||||
<AutoHideScrollbar>
|
||||
{ joined }
|
||||
{ invited }
|
||||
</AutoHideScrollbar>
|
||||
{ inputBox }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@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 PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.groups.GroupMemberTile")
|
||||
export default class GroupMemberTile extends React.Component {
|
||||
static propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
member: GroupMemberType.isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
onClick = e => {
|
||||
dis.dispatch({
|
||||
action: 'view_group_user',
|
||||
member: this.props.member,
|
||||
groupId: this.props.groupId,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EntityTile = sdk.getComponent('rooms.EntityTile');
|
||||
|
||||
const name = this.props.member.displayname || this.props.member.userId;
|
||||
const avatarUrl = this.props.member.avatarUrl
|
||||
? mediaFromMxc(this.props.member.avatarUrl).getSquareThumbnailHttp(36)
|
||||
: null;
|
||||
|
||||
const av = (
|
||||
<BaseAvatar
|
||||
aria-hidden="true"
|
||||
name={this.props.member.displayname || this.props.member.userId}
|
||||
idName={this.props.member.userId}
|
||||
width={36}
|
||||
height={36}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityTile
|
||||
name={name}
|
||||
avatarJsx={av}
|
||||
onClick={this.onClick}
|
||||
suppressOnHover={true}
|
||||
presenceState="online"
|
||||
powerStatus={this.props.member.isPrivileged ? EntityTile.POWER_STATUS_ADMIN : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.groups.GroupPublicityTile")
|
||||
export default class GroupPublicityToggle extends React.Component {
|
||||
static propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
busy: false,
|
||||
ready: false,
|
||||
isGroupPublicised: false, // assume false as <ToggleSwitch /> expects a boolean
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._initGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
this._groupStoreToken = GroupStore.registerListener(groupId, () => {
|
||||
this.setState({
|
||||
isGroupPublicised: Boolean(GroupStore.getGroupPublicity(groupId)),
|
||||
ready: GroupStore.isStateReady(groupId, GroupStore.STATE_KEY.Summary),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._groupStoreToken) this._groupStoreToken.unregister();
|
||||
}
|
||||
|
||||
_onPublicityToggle = () => {
|
||||
this.setState({
|
||||
busy: true,
|
||||
// Optimistic early update
|
||||
isGroupPublicised: !this.state.isGroupPublicised,
|
||||
});
|
||||
GroupStore.setGroupPublicity(this.props.groupId, !this.state.isGroupPublicised).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
return <div className="mx_GroupPublicity_toggle">
|
||||
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
|
||||
<ToggleSwitch checked={this.state.isGroupPublicised}
|
||||
disabled={!this.state.ready || this.state.busy}
|
||||
onChange={this._onPublicityToggle} />
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,236 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.groups.GroupRoomInfo")
|
||||
export default class GroupRoomInfo extends React.Component {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
static propTypes = {
|
||||
groupId: PropTypes.string,
|
||||
groupRoomId: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
isUserPrivilegedInGroup: null,
|
||||
groupRoom: null,
|
||||
groupRoomPublicityLoading: false,
|
||||
groupRoomRemoveLoading: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._initGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.groupId !== this.props.groupId) {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
this._initGroupStore(newProps.groupId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unregisterGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
_unregisterGroupStore(groupId) {
|
||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
_updateGroupRoom() {
|
||||
this.setState({
|
||||
groupRoom: GroupStore.getGroupRooms(this.props.groupId).find(
|
||||
(r) => r.roomId === this.props.groupRoomId,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
onGroupStoreUpdated = () => {
|
||||
this.setState({
|
||||
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
|
||||
});
|
||||
this._updateGroupRoom();
|
||||
};
|
||||
|
||||
_onRemove = e => {
|
||||
const groupId = this.props.groupId;
|
||||
const roomName = this.state.groupRoom.displayname;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, {
|
||||
title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", { roomName, groupId }),
|
||||
description: _t("Removing a room from the community will also remove it from the community page."),
|
||||
button: _t("Remove"),
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
this.setState({ groupRoomRemoveLoading: true });
|
||||
const groupId = this.props.groupId;
|
||||
const roomId = this.props.groupRoomId;
|
||||
GroupStore.removeRoomFromGroup(this.props.groupId, roomId).then(() => {
|
||||
dis.dispatch({
|
||||
action: "view_group_room_list",
|
||||
});
|
||||
}).catch((err) => {
|
||||
logger.error(`Error whilst removing ${roomId} from ${groupId}`, err);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
||||
title: _t("Failed to remove room from community"),
|
||||
description: _t(
|
||||
"Failed to remove '%(roomName)s' from %(groupId)s", { groupId, roomName },
|
||||
),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({ groupRoomRemoveLoading: false });
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
_onCancel = e => {
|
||||
dis.dispatch({
|
||||
action: "view_group_room_list",
|
||||
});
|
||||
};
|
||||
|
||||
_changeGroupRoomPublicity = e => {
|
||||
const isPublic = e.target.value === "public";
|
||||
this.setState({
|
||||
groupRoomPublicityLoading: true,
|
||||
});
|
||||
const groupId = this.props.groupId;
|
||||
const roomId = this.props.groupRoomId;
|
||||
const roomName = this.state.groupRoom.displayname;
|
||||
GroupStore.updateGroupRoomVisibility(this.props.groupId, roomId, isPublic).catch((err) => {
|
||||
logger.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
|
||||
title: _t("Something went wrong!"),
|
||||
description: _t(
|
||||
"The visibility of '%(roomName)s' in %(groupId)s could not be updated.",
|
||||
{ roomName, groupId },
|
||||
),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
groupRoomPublicityLoading: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <div className="mx_MemberInfo">
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
let adminTools;
|
||||
if (this.state.isUserPrivilegedInGroup) {
|
||||
adminTools =
|
||||
<div className="mx_MemberInfo_adminTools">
|
||||
<h3>{ _t("Admin Tools") }</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
<AccessibleButton className="mx_MemberInfo_field" onClick={this._onRemove}>
|
||||
{ _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<h3>
|
||||
{ _t('Visibility in Room List') }
|
||||
{ this.state.groupRoomPublicityLoading ?
|
||||
<InlineSpinner /> : <div />
|
||||
}
|
||||
</h3>
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value="public"
|
||||
checked={this.state.groupRoom.isPublic}
|
||||
onChange={this._changeGroupRoomPublicity}
|
||||
/>
|
||||
<div className="mx_MemberInfo_label_text">
|
||||
{ _t('Visible to everyone') }
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio"
|
||||
value="private"
|
||||
checked={!this.state.groupRoom.isPublic}
|
||||
onChange={this._changeGroupRoomPublicity}
|
||||
/>
|
||||
<div className="mx_MemberInfo_label_text">
|
||||
{ _t('Only visible to community members') }
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarUrl = this.state.groupRoom.avatarUrl;
|
||||
let avatarElement;
|
||||
if (avatarUrl) {
|
||||
const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800);
|
||||
avatarElement = <div className="mx_MemberInfo_avatar"><img src={httpUrl} /></div>;
|
||||
}
|
||||
|
||||
const groupRoomName = this.state.groupRoom.displayname;
|
||||
return (
|
||||
<div className="mx_MemberInfo" role="tabpanel">
|
||||
<AutoHideScrollbar>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src={require("../../../../res/img/cancel.svg").default} width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
{ avatarElement }
|
||||
|
||||
<h2>{ groupRoomName }</h2>
|
||||
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ this.state.groupRoom.canonicalAlias }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ adminTools }
|
||||
</AutoHideScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import { showGroupAddRoomDialog } from '../../../GroupAddressPicker';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const INITIAL_LOAD_NUM_ROOMS = 30;
|
||||
|
||||
@replaceableComponent("views.groups.GroupRoomList")
|
||||
export default class GroupRoomList extends React.Component {
|
||||
static propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
rooms: null,
|
||||
truncateAt: INITIAL_LOAD_NUM_ROOMS,
|
||||
searchQuery: "",
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
this._initGroupStore(this.props.groupId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
this._unregisterGroupStore();
|
||||
}
|
||||
|
||||
_unregisterGroupStore() {
|
||||
GroupStore.unregisterListener(this.onGroupStoreUpdated);
|
||||
}
|
||||
|
||||
_initGroupStore(groupId) {
|
||||
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
|
||||
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
|
||||
// XXX: This is also leaked - we should remove it when unmounting
|
||||
GroupStore.on('error', (err, errorGroupId) => {
|
||||
if (errorGroupId !== groupId) return;
|
||||
this.setState({
|
||||
rooms: null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onGroupStoreUpdated = () => {
|
||||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
rooms: GroupStore.getGroupRooms(this.props.groupId),
|
||||
});
|
||||
};
|
||||
|
||||
_createOverflowTile = (overflowCount, totalCount) => {
|
||||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
const EntityTile = sdk.getComponent("rooms.EntityTile");
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile
|
||||
className="mx_EntityTile_ellipsis"
|
||||
avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." width={36} height={36} />
|
||||
}
|
||||
name={text}
|
||||
presenceState="online"
|
||||
suppressOnHover={true}
|
||||
onClick={this._showFullRoomList}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_showFullRoomList = () => {
|
||||
this.setState({
|
||||
truncateAt: -1,
|
||||
});
|
||||
};
|
||||
|
||||
onSearchQueryChanged = ev => {
|
||||
this.setState({ searchQuery: ev.target.value });
|
||||
};
|
||||
|
||||
onAddRoomToGroupButtonClick = () => {
|
||||
showGroupAddRoomDialog(this.props.groupId).then(() => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
};
|
||||
|
||||
makeGroupRoomTiles(query) {
|
||||
const GroupRoomTile = sdk.getComponent("groups.GroupRoomTile");
|
||||
query = (query || "").toLowerCase();
|
||||
|
||||
let roomList = this.state.rooms;
|
||||
if (query) {
|
||||
roomList = roomList.filter((room) => {
|
||||
const matchesName = (room.name || "").toLowerCase().includes(query);
|
||||
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
|
||||
return matchesName || matchesAlias;
|
||||
});
|
||||
}
|
||||
|
||||
roomList = roomList.map((groupRoom, index) => {
|
||||
return (
|
||||
<GroupRoomTile
|
||||
key={index}
|
||||
groupId={this.props.groupId}
|
||||
groupRoom={groupRoom} />
|
||||
);
|
||||
});
|
||||
|
||||
return roomList;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.rooms === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let inviteButton;
|
||||
if (GroupStore.isUserPrivileged(this.props.groupId)) {
|
||||
inviteButton = (
|
||||
<AccessibleButton
|
||||
className="mx_MemberList_invite mx_MemberList_addRoomToCommunity"
|
||||
onClick={this.onAddRoomToGroupButtonClick}
|
||||
>
|
||||
<span>{ _t('Add rooms to this community') }</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
const inputBox = (
|
||||
<input
|
||||
className="mx_GroupRoomList_query mx_textinput"
|
||||
id="mx_GroupRoomList_query"
|
||||
type="text"
|
||||
onChange={this.onSearchQueryChanged}
|
||||
value={this.state.searchQuery}
|
||||
placeholder={_t('Filter community rooms')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return (
|
||||
<div className="mx_GroupRoomList" role="tabpanel">
|
||||
{ inviteButton }
|
||||
<AutoHideScrollbar className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
|
||||
<TruncatedList
|
||||
className="mx_GroupRoomList_wrapper"
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}
|
||||
>
|
||||
{ this.makeGroupRoomTiles(this.state.searchQuery) }
|
||||
</TruncatedList>
|
||||
</AutoHideScrollbar>
|
||||
{ inputBox }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { GroupRoomType } from '../../../groups';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
@replaceableComponent("views.groups.GroupRoomTile")
|
||||
class GroupRoomTile extends React.Component {
|
||||
static propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
groupRoom: GroupRoomType.isRequired,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
onClick = e => {
|
||||
dis.dispatch({
|
||||
action: 'view_group_room',
|
||||
groupId: this.props.groupId,
|
||||
groupRoomId: this.props.groupRoom.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const avatarUrl = this.props.groupRoom.avatarUrl
|
||||
? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36)
|
||||
: null;
|
||||
|
||||
const av = (
|
||||
<BaseAvatar
|
||||
name={this.props.groupRoom.displayname}
|
||||
width={36}
|
||||
height={36}
|
||||
url={avatarUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_GroupRoomTile" onClick={this.onClick}>
|
||||
<div className="mx_GroupRoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
<div className="mx_GroupRoomTile_name">
|
||||
{ this.props.groupRoom.displayname }
|
||||
</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupRoomTile;
|
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||
|
||||
@replaceableComponent("views.groups.GroupTile")
|
||||
class GroupTile extends React.Component {
|
||||
static propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
// Whether to show the short description of the group on the tile
|
||||
showDescription: PropTypes.bool,
|
||||
// Height of the group avatar in pixels
|
||||
avatarHeight: PropTypes.number,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
static defaultProps = {
|
||||
showDescription: true,
|
||||
avatarHeight: 50,
|
||||
};
|
||||
|
||||
state = {
|
||||
profile: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => {
|
||||
this.setState({ profile });
|
||||
}).catch((err) => {
|
||||
logger.error('Error whilst getting cached profile for GroupTile', err);
|
||||
});
|
||||
}
|
||||
|
||||
onClick = e => {
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.groupId,
|
||||
});
|
||||
};
|
||||
|
||||
onPinClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0));
|
||||
};
|
||||
|
||||
onUnpinClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId));
|
||||
};
|
||||
|
||||
render() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.groupId;
|
||||
const avatarHeight = this.props.avatarHeight;
|
||||
const descElement = this.props.showDescription ?
|
||||
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
|
||||
<div />;
|
||||
const httpUrl = profile.avatarUrl
|
||||
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
|
||||
: null;
|
||||
|
||||
const avatarElement = (
|
||||
<div className="mx_GroupTile_avatar">
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
idName={this.props.groupId}
|
||||
url={httpUrl}
|
||||
width={avatarHeight}
|
||||
height={avatarHeight} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
|
||||
{ avatarElement }
|
||||
<div className="mx_GroupTile_profile">
|
||||
<div className="mx_GroupTile_name">{ name }</div>
|
||||
{ descElement }
|
||||
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
|
||||
{ !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId)
|
||||
? <AccessibleButton kind="link" onClick={this.onPinClick}>
|
||||
{ _t("Pin") }
|
||||
</AccessibleButton>
|
||||
: <AccessibleButton kind="link" onClick={this.onUnpinClick}>
|
||||
{ _t("Unpin") }
|
||||
</AccessibleButton>
|
||||
}
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupTile;
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.groups.GroupUserSettings")
|
||||
export default class GroupUserSettings extends React.Component {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
state = {
|
||||
error: null,
|
||||
groups: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.context.getJoinedGroups().then((result) => {
|
||||
this.setState({ groups: result.groups || [], error: null });
|
||||
}, (err) => {
|
||||
logger.error(err);
|
||||
this.setState({ groups: null, error: err });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let text = "";
|
||||
let groupPublicityToggles = null;
|
||||
const groups = this.state.groups;
|
||||
|
||||
if (this.state.error) {
|
||||
text = _t('Something went wrong when trying to get your communities.');
|
||||
} else if (groups === null) {
|
||||
text = _t('Loading...');
|
||||
} else if (groups.length > 0) {
|
||||
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
|
||||
groupPublicityToggles = groups.map((groupId, index) => {
|
||||
return <GroupPublicityToggle key={index} groupId={groupId} />;
|
||||
});
|
||||
text = _t('Display your community flair in rooms configured to show it.');
|
||||
} else {
|
||||
text = _t("You're not currently a member of any communities.");
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className="mx_SettingsTab_subsectionText">{ text }</p>
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{ groupPublicityToggles }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,6 @@ import UserIdentifier from "../../../customisations/UserIdentifier";
|
|||
interface IProps {
|
||||
member?: RoomMember;
|
||||
fallbackName: string;
|
||||
flair?: JSX.Element;
|
||||
onClick?(): void;
|
||||
colored?: boolean;
|
||||
emphasizeDisplayName?: boolean;
|
||||
|
@ -33,7 +32,7 @@ interface IProps {
|
|||
|
||||
export default class DisambiguatedProfile extends React.Component<IProps> {
|
||||
render() {
|
||||
const { fallbackName, member, flair, colored, emphasizeDisplayName, onClick } = this.props;
|
||||
const { fallbackName, member, colored, emphasizeDisplayName, onClick } = this.props;
|
||||
const rawDisplayName = member?.rawDisplayName || fallbackName;
|
||||
const mxid = member?.userId;
|
||||
|
||||
|
@ -64,7 +63,6 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
|
|||
{ rawDisplayName }
|
||||
</span>
|
||||
{ mxidElement }
|
||||
{ flair }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,6 @@ export default class MImageReplyBody extends MImageBody {
|
|||
const fileBody = this.getFileBody();
|
||||
const sender = <SenderProfile
|
||||
mxEvent={this.props.mxEvent}
|
||||
enableFlair={false}
|
||||
/>;
|
||||
|
||||
return <div className="mx_MImageReplyBody">
|
||||
|
|
|
@ -17,10 +17,7 @@
|
|||
import React from 'react';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
|
||||
import Flair from '../elements/Flair';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import DisambiguatedProfile from "./DisambiguatedProfile";
|
||||
|
@ -31,78 +28,12 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|||
interface IProps {
|
||||
mxEvent: MatrixEvent;
|
||||
onClick?(): void;
|
||||
enableFlair: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
userGroups: string[];
|
||||
relatedGroups: string[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.messages.SenderProfile")
|
||||
export default class SenderProfile extends React.Component<IProps, IState> {
|
||||
static contextType = MatrixClientContext;
|
||||
export default class SenderProfile extends React.PureComponent<IProps> {
|
||||
public static contextType = MatrixClientContext;
|
||||
public context!: React.ContextType<typeof MatrixClientContext>;
|
||||
private unmounted = false;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
const senderId = this.props.mxEvent.getSender();
|
||||
|
||||
this.state = {
|
||||
userGroups: FlairStore.cachedPublicisedGroups(senderId) || [],
|
||||
relatedGroups: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateRelatedGroups();
|
||||
|
||||
if (this.state.userGroups.length === 0) {
|
||||
this.getPublicisedGroups();
|
||||
}
|
||||
|
||||
this.context.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
}
|
||||
|
||||
private async getPublicisedGroups() {
|
||||
const userGroups = await FlairStore.getPublicisedGroupsCached(this.context, this.props.mxEvent.getSender());
|
||||
if (this.unmounted) return;
|
||||
this.setState({ userGroups });
|
||||
}
|
||||
|
||||
private onRoomStateEvents = (event: MatrixEvent) => {
|
||||
if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId()) {
|
||||
this.updateRelatedGroups();
|
||||
}
|
||||
};
|
||||
|
||||
private updateRelatedGroups() {
|
||||
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||
this.setState({
|
||||
relatedGroups: relatedGroupsEvent?.getContent().groups || [],
|
||||
});
|
||||
}
|
||||
|
||||
private getDisplayedGroups(userGroups?: string[], relatedGroups?: string[]) {
|
||||
let displayedGroups = userGroups || [];
|
||||
if (relatedGroups && relatedGroups.length > 0) {
|
||||
displayedGroups = relatedGroups.filter((groupId) => {
|
||||
return displayedGroups.includes(groupId);
|
||||
});
|
||||
} else {
|
||||
displayedGroups = [];
|
||||
}
|
||||
return displayedGroups;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mxEvent, onClick } = this.props;
|
||||
|
@ -124,19 +55,9 @@ export default class SenderProfile extends React.Component<IProps, IState> {
|
|||
return null; // emote message must include the name so don't duplicate it
|
||||
}
|
||||
|
||||
let flair;
|
||||
if (this.props.enableFlair) {
|
||||
const displayedGroups = this.getDisplayedGroups(
|
||||
this.state.userGroups, this.state.relatedGroups,
|
||||
);
|
||||
|
||||
flair = <Flair key='flair' userId={mxEvent.getSender()} groups={displayedGroups} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DisambiguatedProfile
|
||||
fallbackName={mxEvent.getSender() || ""}
|
||||
flair={flair}
|
||||
onClick={onClick}
|
||||
member={member}
|
||||
colored={true}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import HeaderButtons, { HeaderKind } from './HeaderButtons';
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
|
||||
|
||||
const GROUP_PHASES = [
|
||||
RightPanelPhases.GroupMemberInfo,
|
||||
RightPanelPhases.GroupMemberList,
|
||||
];
|
||||
const ROOM_PHASES = [
|
||||
RightPanelPhases.GroupRoomList,
|
||||
RightPanelPhases.GroupRoomInfo,
|
||||
];
|
||||
|
||||
interface IProps {}
|
||||
|
||||
@replaceableComponent("views.right_panel.GroupHeaderButtons")
|
||||
export default class GroupHeaderButtons extends HeaderButtons {
|
||||
constructor(props: IProps) {
|
||||
super(props, HeaderKind.Group);
|
||||
}
|
||||
|
||||
protected onAction(payload: ActionPayload) {
|
||||
if (payload.action === Action.ViewUser) {
|
||||
if ((payload as ViewUserPayload).member) {
|
||||
RightPanelStore.instance.setCards([
|
||||
{ phase: RightPanelPhases.GroupRoomInfo },
|
||||
{ phase: RightPanelPhases.GroupMemberList },
|
||||
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } },
|
||||
]);
|
||||
} else {
|
||||
this.setPhase(RightPanelPhases.GroupMemberList);
|
||||
}
|
||||
} else if (payload.action === "view_group") {
|
||||
this.setPhase(RightPanelPhases.GroupMemberList);
|
||||
} else if (payload.action === "view_group_room") {
|
||||
this.setPhase(
|
||||
RightPanelPhases.GroupRoomInfo,
|
||||
{ groupRoomId: payload.groupRoomId, groupId: payload.groupId },
|
||||
);
|
||||
} else if (payload.action === "view_group_room_list") {
|
||||
this.setPhase(RightPanelPhases.GroupRoomList);
|
||||
} else if (payload.action === "view_group_member_list") {
|
||||
this.setPhase(RightPanelPhases.GroupMemberList);
|
||||
} else if (payload.action === "view_group_user") {
|
||||
this.setPhase(RightPanelPhases.GroupMemberInfo, { member: payload.member });
|
||||
}
|
||||
}
|
||||
|
||||
private onMembersClicked = () => {
|
||||
if (this.state.phase === RightPanelPhases.GroupMemberInfo) {
|
||||
// send the active phase to trigger a toggle
|
||||
this.setPhase(RightPanelPhases.GroupMemberInfo);
|
||||
} else {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.GroupMemberList);
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomsClicked = () => {
|
||||
// This toggles for us, if needed
|
||||
this.setPhase(RightPanelPhases.GroupRoomList);
|
||||
};
|
||||
|
||||
renderButtons() {
|
||||
return <>
|
||||
<HeaderButton
|
||||
name="groupMembersButton"
|
||||
title={_t('Members')}
|
||||
isHighlighted={this.isPhase(GROUP_PHASES)}
|
||||
onClick={this.onMembersClicked}
|
||||
analytics={['Right Panel', 'Group Member List Button', 'click']}
|
||||
/>
|
||||
<HeaderButton
|
||||
name="roomsButton"
|
||||
title={_t('Rooms')}
|
||||
isHighlighted={this.isPhase(ROOM_PHASES)}
|
||||
onClick={this.onRoomsClicked}
|
||||
analytics={['Right Panel', 'Group Room List Button', 'click']}
|
||||
/>
|
||||
</>;
|
||||
}
|
||||
}
|
|
@ -30,7 +30,6 @@ import { NotificationColor } from '../../../stores/notifications/NotificationCol
|
|||
|
||||
export enum HeaderKind {
|
||||
Room = "room",
|
||||
Group = "group",
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -91,9 +90,7 @@ export default abstract class HeaderButtons<P = {}> extends React.Component<IPro
|
|||
|
||||
private onRightPanelStoreUpdate = () => {
|
||||
if (this.unmounted) return;
|
||||
let phase = RightPanelStore.instance.currentCard.phase;
|
||||
if (!RightPanelStore.instance.isOpenForGroup) {phase = null;}
|
||||
this.setState({ phase });
|
||||
this.setState({ phase: RightPanelStore.instance.currentCard.phase });
|
||||
};
|
||||
|
||||
// XXX: Make renderButtons a prop
|
||||
|
|
|
@ -39,7 +39,6 @@ import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
|||
import SdkConfig from '../../../SdkConfig';
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import MultiInviter from "../../../utils/MultiInviter";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import E2EIcon from "../rooms/E2EIcon";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
|
@ -69,7 +68,6 @@ import RoomName from "../elements/RoomName";
|
|||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
|
||||
import { bulkSpaceBehaviour } from "../../../utils/space";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
|
@ -753,7 +751,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(SpaceStore.spacesEnabled && room?.isSpaceRoom()))) return;
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
logger.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
|
@ -847,7 +845,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
stopUpdating={stopUpdating}
|
||||
/>;
|
||||
}
|
||||
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
|
||||
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
|
@ -885,99 +883,6 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
return <div />;
|
||||
};
|
||||
|
||||
export interface GroupMember {
|
||||
userId: string;
|
||||
displayname?: string; // XXX: GroupMember objects are inconsistent :((
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
const GroupAdminToolsSection: React.FC<{
|
||||
groupId: string;
|
||||
groupMember: GroupMember;
|
||||
startUpdating(): void;
|
||||
stopUpdating(): void;
|
||||
}> = ({ children, groupId, groupMember, startUpdating, stopUpdating }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const [isPrivileged, setIsPrivileged] = useState(false);
|
||||
const [isInvited, setIsInvited] = useState(false);
|
||||
|
||||
// Listen to group store changes
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
|
||||
const onGroupStoreUpdated = () => {
|
||||
if (unmounted) return;
|
||||
setIsPrivileged(GroupStore.isUserPrivileged(groupId));
|
||||
setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some(
|
||||
(m) => m.userId === groupMember.userId,
|
||||
));
|
||||
};
|
||||
|
||||
GroupStore.registerListener(groupId, onGroupStoreUpdated);
|
||||
onGroupStoreUpdated();
|
||||
// Handle unmount
|
||||
return () => {
|
||||
unmounted = true;
|
||||
GroupStore.unregisterListener(onGroupStoreUpdated);
|
||||
};
|
||||
}, [groupId, groupMember.userId]);
|
||||
|
||||
if (isPrivileged) {
|
||||
const onKick = async () => {
|
||||
const { finished } = Modal.createDialog(ConfirmUserActionDialog, {
|
||||
matrixClient: cli,
|
||||
groupMember,
|
||||
action: isInvited ? _t('Disinvite') : _t('Remove from community'),
|
||||
title: isInvited ? _t('Disinvite this user from community?')
|
||||
: _t('Remove this user from community?'),
|
||||
danger: true,
|
||||
});
|
||||
|
||||
const [proceed] = await finished;
|
||||
if (!proceed) return;
|
||||
|
||||
startUpdating();
|
||||
cli.removeUserFromGroup(groupId, groupMember.userId).then(() => {
|
||||
// return to the user list
|
||||
dis.dispatch({
|
||||
action: Action.ViewUser,
|
||||
member: null,
|
||||
});
|
||||
}).catch((e) => {
|
||||
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
|
||||
title: _t('Error'),
|
||||
description: isInvited ?
|
||||
_t('Failed to withdraw invitation') :
|
||||
_t('Failed to remove user from community'),
|
||||
});
|
||||
logger.log(e);
|
||||
}).finally(() => {
|
||||
stopUpdating();
|
||||
});
|
||||
};
|
||||
|
||||
const kickButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
|
||||
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
// No make/revoke admin API yet
|
||||
/*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
|
||||
giveModButton = <AccessibleButton className="mx_UserInfo_field" onClick={this.onModToggle}>
|
||||
{giveOpLabel}
|
||||
</AccessibleButton>;*/
|
||||
|
||||
return <GenericAdminToolsContainer>
|
||||
{ kickButton }
|
||||
{ children }
|
||||
</GenericAdminToolsContainer>;
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
const useIsSynapseAdmin = (cli: MatrixClient) => {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
useEffect(() => {
|
||||
|
@ -1135,7 +1040,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(SpaceStore.spacesEnabled && room?.isSpaceRoom()))) return;
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
logger.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
|
@ -1233,10 +1138,9 @@ export const useDevices = (userId: string) => {
|
|||
const BasicUserInfo: React.FC<{
|
||||
room: Room;
|
||||
member: User | RoomMember;
|
||||
groupId: string;
|
||||
devices: IDevice[];
|
||||
isRoomEncrypted: boolean;
|
||||
}> = ({ room, member, groupId, devices, isRoomEncrypted }) => {
|
||||
}> = ({ room, member, devices, isRoomEncrypted }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
const powerLevels = useRoomPowerLevels(cli, room);
|
||||
|
@ -1338,16 +1242,6 @@ const BasicUserInfo: React.FC<{
|
|||
{ synapseDeactivateButton }
|
||||
</RoomAdminToolsContainer>
|
||||
);
|
||||
} else if (groupId) {
|
||||
adminToolsContainer = (
|
||||
<GroupAdminToolsSection
|
||||
groupId={groupId}
|
||||
groupMember={member}
|
||||
startUpdating={startUpdating}
|
||||
stopUpdating={stopUpdating}>
|
||||
{ synapseDeactivateButton }
|
||||
</GroupAdminToolsSection>
|
||||
);
|
||||
} else if (synapseDeactivateButton) {
|
||||
adminToolsContainer = (
|
||||
<GenericAdminToolsContainer>
|
||||
|
@ -1367,10 +1261,10 @@ const BasicUserInfo: React.FC<{
|
|||
if (!isRoomEncrypted) {
|
||||
if (!cryptoEnabled) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
|
||||
} else if (room && !room.isSpaceRoom()) {
|
||||
text = _t("Messages in this room are not end-to-end encrypted.");
|
||||
}
|
||||
} else if (!SpaceStore.spacesEnabled || !room.isSpaceRoom()) {
|
||||
} else if (!room.isSpaceRoom()) {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
|
@ -1452,7 +1346,7 @@ const BasicUserInfo: React.FC<{
|
|||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member as RoomMember}
|
||||
isSpace={SpaceStore.spacesEnabled && room?.isSpaceRoom()}
|
||||
isSpace={room?.isSpaceRoom()}
|
||||
/>
|
||||
|
||||
{ adminToolsContainer }
|
||||
|
@ -1461,7 +1355,7 @@ const BasicUserInfo: React.FC<{
|
|||
</React.Fragment>;
|
||||
};
|
||||
|
||||
export type Member = User | RoomMember | GroupMember;
|
||||
export type Member = User | RoomMember;
|
||||
|
||||
const UserInfoHeader: React.FC<{
|
||||
member: Member;
|
||||
|
@ -1540,7 +1434,7 @@ const UserInfoHeader: React.FC<{
|
|||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||
}
|
||||
|
||||
const displayName = (member as RoomMember).rawDisplayName || (member as GroupMember).displayname;
|
||||
const displayName = (member as RoomMember).rawDisplayName;
|
||||
return <React.Fragment>
|
||||
{ avatarElement }
|
||||
|
||||
|
@ -1566,10 +1460,8 @@ const UserInfoHeader: React.FC<{
|
|||
|
||||
interface IProps {
|
||||
user: Member;
|
||||
groupId?: string;
|
||||
room?: Room;
|
||||
phase: RightPanelPhases.RoomMemberInfo
|
||||
| RightPanelPhases.GroupMemberInfo
|
||||
| RightPanelPhases.SpaceMemberInfo
|
||||
| RightPanelPhases.EncryptionPanel;
|
||||
onClose(): void;
|
||||
|
@ -1579,7 +1471,6 @@ interface IProps {
|
|||
|
||||
const UserInfo: React.FC<IProps> = ({
|
||||
user,
|
||||
groupId,
|
||||
room,
|
||||
onClose,
|
||||
phase = RightPanelPhases.RoomMemberInfo,
|
||||
|
@ -1601,10 +1492,10 @@ const UserInfo: React.FC<IProps> = ({
|
|||
const classes = ["mx_UserInfo"];
|
||||
|
||||
let cardState: IRightPanelCardState;
|
||||
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
|
||||
// We have no previousPhase for when viewing a UserInfo without a Room at this time
|
||||
if (room && phase === RightPanelPhases.EncryptionPanel) {
|
||||
cardState = { member };
|
||||
} else if (room?.isSpaceRoom() && SpaceStore.spacesEnabled) {
|
||||
} else if (room?.isSpaceRoom()) {
|
||||
cardState = { spaceId: room.roomId };
|
||||
}
|
||||
|
||||
|
@ -1615,13 +1506,11 @@ const UserInfo: React.FC<IProps> = ({
|
|||
let content;
|
||||
switch (phase) {
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
case RightPanelPhases.GroupMemberInfo:
|
||||
case RightPanelPhases.SpaceMemberInfo:
|
||||
content = (
|
||||
<BasicUserInfo
|
||||
room={room}
|
||||
member={member as User}
|
||||
groupId={groupId as string}
|
||||
devices={devices}
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
/>
|
||||
|
@ -1649,7 +1538,7 @@ const UserInfo: React.FC<IProps> = ({
|
|||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) {
|
||||
if (room?.isSpaceRoom()) {
|
||||
scopeHeader = <div data-test-id='space-header' className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
/*
|
||||
Copyright 2017, 2019 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
|
||||
const GROUP_ID_REGEX = /\+\S+:\S+/;
|
||||
|
||||
@replaceableComponent("views.room_settings.RelatedGroupSettings")
|
||||
export default class RelatedGroupSettings extends React.Component {
|
||||
static propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
canSetRelatedGroups: PropTypes.bool.isRequired,
|
||||
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
static defaultProps = {
|
||||
canSetRelatedGroups: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
newGroupId: "",
|
||||
newGroupsList: props.relatedGroupsEvent ? (props.relatedGroupsEvent.getContent().groups || []) : [],
|
||||
};
|
||||
}
|
||||
|
||||
updateGroups(newGroupsList) {
|
||||
this.context.sendStateEvent(this.props.roomId, 'm.room.related_groups', {
|
||||
groups: newGroupsList,
|
||||
}, '').catch((err) => {
|
||||
logger.error(err);
|
||||
Modal.createTrackedDialog('Error updating flair', '', ErrorDialog, {
|
||||
title: _t("Error updating flair"),
|
||||
description: _t(
|
||||
"There was an error updating the flair for this room. The server may not allow it or " +
|
||||
"a temporary error occurred.",
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
validateGroupId(groupId) {
|
||||
if (!GROUP_ID_REGEX.test(groupId)) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Invalid related community ID', '', ErrorDialog, {
|
||||
title: _t('Invalid community ID'),
|
||||
description: _t('\'%(groupId)s\' is not a valid community ID', { groupId }),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onNewGroupChanged = (newGroupId) => {
|
||||
this.setState({ newGroupId });
|
||||
};
|
||||
|
||||
onGroupAdded = (groupId) => {
|
||||
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
|
||||
return;
|
||||
}
|
||||
const newGroupsList = [...this.state.newGroupsList, groupId];
|
||||
this.setState({
|
||||
newGroupsList: newGroupsList,
|
||||
newGroupId: '',
|
||||
});
|
||||
this.updateGroups(newGroupsList);
|
||||
};
|
||||
|
||||
onGroupDeleted = (index) => {
|
||||
const group = this.state.newGroupsList[index];
|
||||
const newGroupsList = this.state.newGroupsList.filter((g) => g !== group);
|
||||
this.setState({ newGroupsList });
|
||||
this.updateGroups(newGroupsList);
|
||||
};
|
||||
|
||||
render() {
|
||||
const localDomain = this.context.getDomain();
|
||||
const EditableItemList = sdk.getComponent('elements.EditableItemList');
|
||||
return <div>
|
||||
<EditableItemList
|
||||
id="relatedGroups"
|
||||
items={this.state.newGroupsList}
|
||||
className="mx_RelatedGroupSettings"
|
||||
newItem={this.state.newGroupId}
|
||||
canRemove={this.props.canSetRelatedGroups}
|
||||
canEdit={this.props.canSetRelatedGroups}
|
||||
onNewItemChanged={this.onNewGroupChanged}
|
||||
onItemAdded={this.onGroupAdded}
|
||||
onItemRemoved={this.onGroupDeleted}
|
||||
itemsLabel={_t('Showing flair for these communities:')}
|
||||
noItemsLabel={_t('This room is not showing flair for any communities')}
|
||||
placeholder={_t(
|
||||
'New community ID (e.g. +foo:%(localDomain)s)', { localDomain },
|
||||
)}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -115,7 +115,6 @@ const stateEventTileTypes = {
|
|||
[EventType.RoomTombstone]: 'messages.TextualEvent',
|
||||
[EventType.RoomJoinRules]: 'messages.TextualEvent',
|
||||
[EventType.RoomGuestAccess]: 'messages.TextualEvent',
|
||||
'm.room.related_groups': 'messages.TextualEvent', // legacy communities flair
|
||||
};
|
||||
|
||||
const stateEventSingular = new Set([
|
||||
|
@ -133,7 +132,6 @@ const stateEventSingular = new Set([
|
|||
EventType.RoomTombstone,
|
||||
EventType.RoomJoinRules,
|
||||
EventType.RoomGuestAccess,
|
||||
'm.room.related_groups',
|
||||
]);
|
||||
|
||||
// Add all the Mjolnir stuff to the renderer
|
||||
|
@ -292,9 +290,6 @@ interface IProps {
|
|||
// which layout to use
|
||||
layout?: Layout;
|
||||
|
||||
// whether or not to show flair at all
|
||||
enableFlair?: boolean;
|
||||
|
||||
// whether or not to show read receipts
|
||||
showReadReceipts?: boolean;
|
||||
|
||||
|
@ -1214,12 +1209,10 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
sender = <SenderProfile
|
||||
onClick={this.onSenderProfileClick}
|
||||
mxEvent={this.props.mxEvent}
|
||||
enableFlair={this.props.enableFlair}
|
||||
/>;
|
||||
} else {
|
||||
sender = <SenderProfile
|
||||
mxEvent={this.props.mxEvent}
|
||||
enableFlair={this.props.enableFlair}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
@ -46,7 +45,6 @@ import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
|||
import EntityTile from "./EntityTile";
|
||||
import MemberTile from "./MemberTile";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
|
@ -523,10 +521,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
|
||||
if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
let inviteButtonText = _t("Invite to this room");
|
||||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat && chat.roomId === this.props.roomId) {
|
||||
inviteButtonText = _t("Invite to this community");
|
||||
} else if (SpaceStore.spacesEnabled && room.isSpaceRoom()) {
|
||||
if (room.isSpaceRoom()) {
|
||||
inviteButtonText = _t("Invite to this space");
|
||||
}
|
||||
|
||||
|
@ -566,7 +561,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
);
|
||||
|
||||
let scopeHeader;
|
||||
if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) {
|
||||
if (room?.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
|
@ -603,7 +598,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
return;
|
||||
}
|
||||
|
||||
// call AddressPickerDialog
|
||||
// open the room inviter
|
||||
dis.dispatch({
|
||||
action: 'view_invite',
|
||||
roomId: this.props.roomId,
|
||||
|
|
|
@ -146,7 +146,6 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
if (needsSenderProfile) {
|
||||
sender = <SenderProfile
|
||||
mxEvent={mxEvent}
|
||||
enableFlair={false}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,18 +25,15 @@ import ResizeNotifier from "../../../utils/ResizeNotifier";
|
|||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
||||
import { DefaultTagID, isCustomTag, TagID } from "../../../stores/room-list/models";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import GroupAvatar from "../avatars/GroupAvatar";
|
||||
import ExtraTile from "./ExtraTile";
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
|
||||
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
|
||||
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
||||
import IconizedContextMenu, {
|
||||
|
@ -44,7 +41,6 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import {
|
||||
isMetaSpace,
|
||||
|
@ -90,15 +86,11 @@ export const TAG_ORDER: TagID[] = [
|
|||
DefaultTagID.Favourite,
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Untagged,
|
||||
|
||||
// -- Custom Tags Placeholder --
|
||||
|
||||
DefaultTagID.LowPriority,
|
||||
DefaultTagID.ServerNotice,
|
||||
DefaultTagID.Suggested,
|
||||
DefaultTagID.Archived,
|
||||
];
|
||||
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
|
||||
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Untagged,
|
||||
|
@ -274,9 +266,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
|||
}}
|
||||
/> }
|
||||
<IconizedContextMenuOption
|
||||
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
|
||||
? _t("Explore community rooms")
|
||||
: _t("Explore public rooms")}
|
||||
label={_t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
@ -359,22 +349,9 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
},
|
||||
};
|
||||
|
||||
function customTagAesthetics(tagId: TagID): ITagAesthetics {
|
||||
if (tagId.startsWith("u.")) {
|
||||
tagId = tagId.substring(2);
|
||||
}
|
||||
return {
|
||||
sectionLabel: _td("Custom Tag"),
|
||||
sectionLabelRaw: tagId,
|
||||
isInvite: false,
|
||||
defaultHidden: false,
|
||||
};
|
||||
}
|
||||
|
||||
@replaceableComponent("views.rooms.RoomList")
|
||||
export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||
private dispatcherRef;
|
||||
private customTagStoreRef;
|
||||
private roomStoreToken: fbEmitter.EventSubscription;
|
||||
private treeRef = createRef<HTMLDivElement>();
|
||||
|
||||
|
@ -396,7 +373,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
|
||||
this.updateLists(); // trigger the first update
|
||||
}
|
||||
|
||||
|
@ -404,7 +380,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
if (this.customTagStoreRef) this.customTagStoreRef.remove();
|
||||
if (this.roomStoreToken) this.roomStoreToken.remove();
|
||||
}
|
||||
|
||||
|
@ -463,12 +438,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
private updateLists = () => {
|
||||
const newLists = RoomListStore.instance.orderedLists;
|
||||
const previousListIds = Object.keys(this.state.sublists);
|
||||
const newListIds = Object.keys(newLists).filter(t => {
|
||||
if (!isCustomTag(t)) return true; // always include non-custom tags
|
||||
|
||||
// if the tag is custom though, only include it if it is enabled
|
||||
return CustomRoomTagStore.getTags()[t];
|
||||
});
|
||||
const newListIds = Object.keys(newLists);
|
||||
|
||||
const isNameFiltering = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||
let doUpdate = this.state.isNameFiltering !== isNameFiltering || arrayHasDiff(previousListIds, newListIds);
|
||||
|
@ -559,68 +529,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
if (SpaceStore.spacesEnabled) return [];
|
||||
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||
// See https://github.com/vector-im/element-web/issues/14456
|
||||
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||
return g.myMembership === 'invite';
|
||||
}).map(g => {
|
||||
const avatar = (
|
||||
<GroupAvatar
|
||||
groupId={g.groupId}
|
||||
groupName={g.name}
|
||||
groupAvatarUrl={g.avatarUrl}
|
||||
width={32}
|
||||
height={32}
|
||||
resizeMethod='crop'
|
||||
/>
|
||||
);
|
||||
const openGroup = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: g.groupId,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<ExtraTile
|
||||
isMinimized={this.props.isMinimized}
|
||||
isSelected={false}
|
||||
displayName={g.name}
|
||||
avatar={avatar}
|
||||
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||
onClick={openGroup}
|
||||
key={`temporaryGroupTile_${g.groupId}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private renderSublists(): React.ReactElement[] {
|
||||
// show a skeleton UI if the user is in no rooms and they are not filtering and have no suggested rooms
|
||||
const showSkeleton = !this.state.isNameFiltering && !this.state.suggestedRooms?.length &&
|
||||
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
||||
|
||||
return TAG_ORDER.reduce((tags, tagId) => {
|
||||
if (tagId === CUSTOM_TAGS_BEFORE_TAG) {
|
||||
const customTags = Object.keys(this.state.sublists)
|
||||
.filter(tagId => isCustomTag(tagId));
|
||||
tags.push(...customTags);
|
||||
}
|
||||
tags.push(tagId);
|
||||
return tags;
|
||||
}, [] as TagID[])
|
||||
return TAG_ORDER
|
||||
.map(orderedTagId => {
|
||||
let extraTiles = null;
|
||||
if (orderedTagId === DefaultTagID.Invite) {
|
||||
extraTiles = this.renderCommunityInvites();
|
||||
} else if (orderedTagId === DefaultTagID.Suggested) {
|
||||
if (orderedTagId === DefaultTagID.Suggested) {
|
||||
extraTiles = this.renderSuggestedRooms();
|
||||
}
|
||||
|
||||
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
|
||||
? customTagAesthetics(orderedTagId)
|
||||
: TAG_AESTHETICS[orderedTagId];
|
||||
const aesthetics = TAG_AESTHETICS[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
let alwaysVisible = ALWAYS_VISIBLE_TAGS.includes(orderedTagId);
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps, useContext, useEffect, useState } from "react";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
|
@ -30,7 +30,6 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import {
|
||||
shouldShowSpaceInvite,
|
||||
showAddExistingRooms,
|
||||
|
@ -38,14 +37,7 @@ import {
|
|||
showCreateNewSubspace,
|
||||
showSpaceInvite,
|
||||
} from "../../../utils/space";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import EditCommunityPrototypeDialog from "../dialogs/EditCommunityPrototypeDialog";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import { showCommunityInviteDialog } from "../../../RoomInvite";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
@ -57,7 +49,6 @@ import {
|
|||
UPDATE_HOME_BEHAVIOUR,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
} from "../../../stores/spaces";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import TooltipTarget from "../elements/TooltipTarget";
|
||||
import { BetaPill } from "../beta/BetaCard";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
|
@ -74,69 +65,6 @@ const contextMenuBelow = (elementRect: DOMRect) => {
|
|||
return { left, top, chevronFace };
|
||||
};
|
||||
|
||||
const PrototypeCommunityContextMenu = (props: ComponentProps<typeof SpaceContextMenu>) => {
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
|
||||
let settingsOption;
|
||||
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
|
||||
const onCommunitySettingsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
|
||||
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
|
||||
});
|
||||
props.onFinished();
|
||||
};
|
||||
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("Community settings")}
|
||||
onClick={onCommunitySettingsClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onCommunityMembersClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// We'd ideally just pop open a right panel with the member list, but the current
|
||||
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
|
||||
// switch to the general room and open the member list there as it should be in sync
|
||||
// anyways.
|
||||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: chat.roomId,
|
||||
metricsTrigger: undefined, // Deprecated groups
|
||||
}, true);
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList }, undefined, chat.roomId);
|
||||
} else {
|
||||
// "This should never happen" clauses go here for the prototype.
|
||||
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
|
||||
title: _t('Failed to find the general chat for this community'),
|
||||
description: _t("Failed to find the general chat for this community"),
|
||||
});
|
||||
}
|
||||
props.onFinished();
|
||||
};
|
||||
|
||||
return <IconizedContextMenu {...props} compact>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ settingsOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={onCommunityMembersClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
};
|
||||
|
||||
// Long-running actions that should trigger a spinner
|
||||
enum PendingActionType {
|
||||
JoinRoom,
|
||||
|
@ -184,11 +112,10 @@ const usePendingActions = (): Map<PendingActionType, Set<string>> => {
|
|||
};
|
||||
|
||||
interface IProps {
|
||||
spacePanelDisabled: boolean;
|
||||
onVisibilityChange?(): void;
|
||||
}
|
||||
|
||||
const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
|
||||
const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [mainMenuDisplayed, mainMenuHandle, openMainMenu, closeMainMenu] = useContextMenu<HTMLDivElement>();
|
||||
const [plusMenuDisplayed, plusMenuHandle, openPlusMenu, closePlusMenu] = useContextMenu<HTMLDivElement>();
|
||||
|
@ -226,11 +153,8 @@ const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
|
|||
return <div className="mx_LeftPanel_roomListFilterCount">
|
||||
{ _t("%(count)s results", { count }) }
|
||||
</div>;
|
||||
} else if (spacePanelDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
const canAddRooms = activeSpace?.currentState?.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||
|
||||
const canCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
|
||||
|
@ -239,15 +163,13 @@ const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
|
|||
// If the user can't do anything on the plus menu, don't show it. This aims to target the
|
||||
// plus menu shown on the Home tab primarily: the user has options to use the menu for
|
||||
// communities and spaces, but is at risk of no options on the Home tab.
|
||||
const canShowPlusMenu = canCreateRooms || canExploreRooms || activeSpace || communityId;
|
||||
const canShowPlusMenu = canCreateRooms || canExploreRooms || activeSpace;
|
||||
|
||||
let contextMenu: JSX.Element;
|
||||
if (mainMenuDisplayed) {
|
||||
let ContextMenuComponent;
|
||||
if (activeSpace) {
|
||||
ContextMenuComponent = SpaceContextMenu;
|
||||
} else if (communityId) {
|
||||
ContextMenuComponent = PrototypeCommunityContextMenu;
|
||||
} else {
|
||||
ContextMenuComponent = HomeButtonContextMenu;
|
||||
}
|
||||
|
@ -271,17 +193,6 @@ const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
|
|||
closePlusMenu();
|
||||
}}
|
||||
/>;
|
||||
} else if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
|
||||
inviteOption = <IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconInvite"
|
||||
label={_t("Invite")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
let createNewRoomOption: JSX.Element;
|
||||
|
@ -413,8 +324,6 @@ const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
|
|||
let title: string;
|
||||
if (activeSpace) {
|
||||
title = spaceName;
|
||||
} else if (communityId) {
|
||||
title = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
} else {
|
||||
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
|
||||
}
|
||||
|
|
|
@ -27,8 +27,6 @@ import dis from '../../../dispatcher/dispatcher';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import InviteReason from "../elements/InviteReason";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
|
@ -116,7 +114,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
|
||||
componentDidMount() {
|
||||
this.checkInvitedEmail();
|
||||
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -125,10 +122,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
|
||||
}
|
||||
|
||||
private async checkInvitedEmail() {
|
||||
// If this is an invite and we've been told what email address was
|
||||
// invited, fetch the user's account emails and discovery bindings so we
|
||||
|
@ -163,13 +156,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onCommunityUpdate = (roomId: string): void => {
|
||||
if (this.props.room && this.props.room.roomId !== roomId) {
|
||||
return;
|
||||
}
|
||||
this.forceUpdate(); // we have nothing to update
|
||||
};
|
||||
|
||||
private getMessageCase(): MessageCase {
|
||||
const isGuest = MatrixClientPeg.get().isGuest();
|
||||
|
||||
|
@ -241,15 +227,8 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
||||
}
|
||||
|
||||
private communityProfile(): { displayName?: string, avatarMxc?: string } {
|
||||
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
|
||||
return { displayName: null, avatarMxc: null };
|
||||
}
|
||||
|
||||
private roomName(atStart = false): string {
|
||||
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
||||
const profile = this.communityProfile();
|
||||
if (profile.displayName) name = profile.displayName;
|
||||
const name = this.props.room ? this.props.room.name : this.props.roomAlias;
|
||||
if (name) {
|
||||
return name;
|
||||
} else if (atStart) {
|
||||
|
@ -465,10 +444,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case MessageCase.Invite: {
|
||||
const oobData = Object.assign({}, this.props.oobData, {
|
||||
avatarUrl: this.communityProfile().avatarMxc,
|
||||
});
|
||||
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
|
||||
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
|
||||
|
||||
const inviteMember = this.getInviteMember();
|
||||
let inviterElement;
|
||||
|
|
|
@ -52,7 +52,6 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
|
||||
import VoiceChannelStore, { VoiceChannelEvent, IJitsiParticipant } from "../../../stores/VoiceChannelStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
|
@ -158,14 +157,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(prevProps.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room?.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
prevProps.room?.off(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
this.props.room?.on(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
}
|
||||
|
@ -186,10 +177,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.notificationState.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
this.props.room?.on(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
CommunityPrototypeStore.instance.on(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -199,20 +186,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
}
|
||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
CommunityPrototypeStore.instance.off(
|
||||
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
|
||||
this.onCommunityUpdate,
|
||||
);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
|
@ -226,11 +205,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onCommunityUpdate = (roomId: string) => {
|
||||
if (roomId !== this.props.room.roomId) return;
|
||||
this.forceUpdate(); // we don't have anything to actually update
|
||||
};
|
||||
|
||||
private onRoomPreviewChanged = (room: Room) => {
|
||||
if (this.props.room && room.roomId === this.props.room.roomId) {
|
||||
this.generatePreview();
|
||||
|
@ -667,12 +641,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
'mx_RoomTile_minimized': this.props.isMinimized,
|
||||
});
|
||||
|
||||
let roomProfile: IRoomProfile = { displayName: null, avatarMxc: null };
|
||||
if (this.props.tag === DefaultTagID.Invite) {
|
||||
roomProfile = CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
|
||||
}
|
||||
|
||||
let name = roomProfile.displayName || this.props.room.name;
|
||||
let name = this.props.room.name;
|
||||
if (typeof name !== 'string') name = '';
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
|
@ -797,7 +766,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
room={this.props.room}
|
||||
avatarSize={32}
|
||||
displayBadge={this.props.isMinimized}
|
||||
oobData={({ avatarUrl: roomProfile.avatarMxc })}
|
||||
tooltipProps={{ tabIndex: isActive ? 0 : -1 }}
|
||||
/>
|
||||
<div className="mx_RoomTile_details">
|
||||
|
|
|
@ -21,7 +21,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import DateSeparator from "../messages/DateSeparator";
|
||||
|
@ -69,7 +68,6 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
const layout = SettingsStore.getValue("layout");
|
||||
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
|
||||
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
|
||||
const enableFlair = SettingsStore.getValue(UIFeature.Flair);
|
||||
|
||||
const timeline = result.context.getTimeline();
|
||||
for (let j = 0; j < timeline.length; j++) {
|
||||
|
@ -121,7 +119,6 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
onHeightChanged={this.props.onHeightChanged}
|
||||
isTwelveHour={isTwelveHour}
|
||||
alwaysShowTimestamps={alwaysShowTimestamps}
|
||||
enableFlair={enableFlair}
|
||||
lastInSection={lastInSection}
|
||||
continuation={continuation}
|
||||
callEventGrouper={this.callEventGroupers.get(mxEv.getContent().call_id)}
|
||||
|
|
|
@ -31,7 +31,6 @@ import RoomName from "../elements/RoomName";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
event: MatrixEvent;
|
||||
|
@ -138,7 +137,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (SpaceStore.spacesEnabled && this.room.isSpaceRoom()) {
|
||||
if (this.room.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={this.room} height={32} width={32} />
|
||||
<RoomName room={this.room} />
|
||||
|
|
|
@ -25,7 +25,6 @@ import SettingsStore from "../../../../../settings/SettingsStore";
|
|||
import { UIFeature } from "../../../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
|
||||
import RelatedGroupSettings from "../../../room_settings/RelatedGroupSettings";
|
||||
import AliasSettings from "../../../room_settings/AliasSettings";
|
||||
import PosthogTrackers from "../../../../../PosthogTrackers";
|
||||
|
||||
|
@ -67,27 +66,10 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
|
|||
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
|
||||
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
|
||||
|
||||
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
|
||||
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
|
||||
|
||||
const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ?
|
||||
<UrlPreviewSettings room={room} /> :
|
||||
null;
|
||||
|
||||
let flairSection;
|
||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||
flairSection = <>
|
||||
<span className='mx_SettingsTab_subheading'>{ _t("Flair") }</span>
|
||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
||||
<RelatedGroupSettings
|
||||
roomId={room.roomId}
|
||||
canSetRelatedGroups={canChangeGroups}
|
||||
relatedGroupsEvent={groupsEvent}
|
||||
/>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
let leaveSection;
|
||||
if (room.getMyMembership() === "join") {
|
||||
leaveSection = <>
|
||||
|
@ -115,7 +97,6 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
|
|||
canonicalAliasEvent={canonicalAliasEv}
|
||||
/>
|
||||
<div className="mx_SettingsTab_heading">{ _t("Other") }</div>
|
||||
{ flairSection }
|
||||
{ urlPreviewSettings }
|
||||
{ leaveSection }
|
||||
</div>
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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 { _t } from "../../../../../languageHandler";
|
||||
import GroupUserSettings from "../../../groups/GroupUserSettings";
|
||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.FlairUserSettingsTab")
|
||||
export default class FlairUserSettingsTab extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
<span className="mx_SettingsTab_heading">{ _t("Flair") }</span>
|
||||
<div className="mx_SettingsTab_section">
|
||||
<GroupUserSettings />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -243,7 +243,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
|||
) }
|
||||
{ _t("Debug logs contain application " +
|
||||
"usage data including your username, the IDs or aliases of " +
|
||||
"the rooms or groups you have visited, which UI elements you " +
|
||||
"the rooms you have visited, which UI elements you " +
|
||||
"last interacted with, and the usernames of other users. " +
|
||||
"They do not contain messages.",
|
||||
) }
|
||||
|
|
|
@ -15,8 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import React from 'react';
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
|
@ -27,18 +26,10 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
|
|||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||
import SettingsFlag from '../../../elements/SettingsFlag';
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import GroupAvatar from "../../../avatars/GroupAvatar";
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import GroupActions from "../../../../../actions/GroupActions";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||
import { createSpaceFromCommunity } from "../../../../../utils/space";
|
||||
import Spinner from "../../../elements/Spinner";
|
||||
import { UserTab } from "../../../dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
|
||||
import { Action } from "../../../../../dispatcher/actions";
|
||||
import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { CreateEventField, IGroupSummary } from '../../../../../@types/groups';
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn(success: boolean): void;
|
||||
|
@ -58,85 +49,6 @@ interface IState {
|
|||
readMarkerOutOfViewThresholdMs: string;
|
||||
}
|
||||
|
||||
type Community = IGroupSummary & {
|
||||
groupId: string;
|
||||
spaceId?: string;
|
||||
};
|
||||
|
||||
const CommunityMigrator = ({ onFinished }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [communities, setCommunities] = useState<Community[]>(null);
|
||||
useEffect(() => {
|
||||
dis.dispatch(GroupActions.fetchJoinedGroups(cli));
|
||||
}, [cli]);
|
||||
useDispatcher(dis, async payload => {
|
||||
if (payload.action === "GroupActions.fetchJoinedGroups.success") {
|
||||
const communities: Community[] = [];
|
||||
|
||||
const migratedSpaceMap = new Map(cli.getRooms().map(room => {
|
||||
const createContent = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent();
|
||||
if (createContent?.[CreateEventField]) {
|
||||
return [createContent[CreateEventField], room.roomId] as [string, string];
|
||||
}
|
||||
}).filter(Boolean));
|
||||
|
||||
for (const groupId of payload.result.groups) {
|
||||
const summary = await cli.getGroupSummary(groupId) as IGroupSummary;
|
||||
if (summary.user.is_privileged) {
|
||||
communities.push({
|
||||
...summary,
|
||||
groupId,
|
||||
spaceId: migratedSpaceMap.get(groupId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setCommunities(communities);
|
||||
}
|
||||
});
|
||||
|
||||
if (!communities) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return <div className="mx_PreferencesUserSettingsTab_CommunityMigrator">
|
||||
{ communities.map(community => (
|
||||
<div key={community.groupId}>
|
||||
<GroupAvatar
|
||||
groupId={community.groupId}
|
||||
groupAvatarUrl={community.profile.avatar_url}
|
||||
groupName={community.profile.name}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{ community.profile.name }
|
||||
<AccessibleButton
|
||||
kind="primary_outline"
|
||||
onClick={() => {
|
||||
if (community.spaceId) {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: community.spaceId,
|
||||
metricsTrigger: undefined, // other
|
||||
});
|
||||
onFinished();
|
||||
} else {
|
||||
createSpaceFromCommunity(cli, community.groupId).then(([spaceId]) => {
|
||||
if (spaceId) {
|
||||
community.spaceId = spaceId;
|
||||
setCommunities([...communities]); // force component re-render
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ community.spaceId ? _t("Open Space") : _t("Create Space") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
)) }
|
||||
</div>;
|
||||
};
|
||||
|
||||
@replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab")
|
||||
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
|
||||
static ROOM_LIST_SETTINGS = [
|
||||
|
@ -147,10 +59,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||
"Spaces.allRoomsInHome",
|
||||
];
|
||||
|
||||
static COMMUNITIES_SETTINGS = [
|
||||
"showCommunitiesInsteadOfSpaces",
|
||||
];
|
||||
|
||||
static KEYBINDINGS_SETTINGS = [
|
||||
'ctrlFForSearch',
|
||||
];
|
||||
|
@ -193,7 +101,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||
'scrollToBottomOnMessageSent',
|
||||
];
|
||||
static GENERAL_SETTINGS = [
|
||||
'TagPanel.enableTagPanel',
|
||||
'promptBeforeInviteUnknownUsers',
|
||||
// Start automatically after startup (electron-only)
|
||||
// Autocomplete delay (niche text box)
|
||||
|
@ -356,19 +263,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT) }
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
|
||||
<p>{ _t("Communities have been archived to make way for Spaces but you can convert your " +
|
||||
"communities into Spaces below. Converting will ensure your conversations get the latest " +
|
||||
"features.") }</p>
|
||||
<details>
|
||||
<summary>{ _t("Show my Communities") }</summary>
|
||||
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
|
||||
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
|
||||
</details>
|
||||
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS, SettingLevel.DEVICE) }
|
||||
</div>
|
||||
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
|
||||
<div className="mx_SettingsFlag">
|
||||
|
|
|
@ -35,9 +35,6 @@ import SdkConfig from "../../../SdkConfig";
|
|||
import Modal from "../../../Modal";
|
||||
import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
|
||||
|
@ -267,14 +264,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
|
||||
let body;
|
||||
if (visibility === null) {
|
||||
const onCreateSpaceFromCommunityClick = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Preferences,
|
||||
});
|
||||
onFinished();
|
||||
};
|
||||
|
||||
body = <React.Fragment>
|
||||
<h2>{ _t("Create a space") }</h2>
|
||||
<p>
|
||||
|
@ -296,12 +285,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
|||
/>
|
||||
|
||||
<p>
|
||||
{ _t("You can also make Spaces from <a>communities</a>.", {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
<br />
|
||||
{ _t("To join a space you'll need an invite.") }
|
||||
</p>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue