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:
Travis Ralston 2022-03-22 17:07:37 -06:00 committed by GitHub
parent 03c80707c9
commit fce36ec826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
171 changed files with 317 additions and 12160 deletions

View file

@ -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}
/>
);
}
}

View file

@ -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>;
}
}

View file

@ -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>;
}
}

View file

@ -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>
);
}
}

View file

@ -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.",
) }

View file

@ -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>
);
}
}

View file

@ -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;

View file

@ -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 },

View file

@ -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*/ }
&nbsp;{ communityId }
</span>
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
{ _t("Create") }
</AccessibleButton>
</div>
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
<input
type="file"
style={{ display: "none" }}
ref={this.avatarUploadRef}
accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton
onClick={this.onChangeAvatar}
className="mx_CreateCommunityPrototypeDialog_avatarContainer"
>
{ preview }
</AccessibleButton>
<div className="mx_CreateCommunityPrototypeDialog_tip">
<b>{ _t("Add image (optional)") }</b>
<span>
{ _t("An image will help people identify your community.") }
</span>
</div>
</div>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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');
}

View file

@ -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>,
}) }
&nbsp;
{ 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.") }
&nbsp;
{ _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;

View file

@ -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>
);
}
}

View file

@ -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"
/>

View file

@ -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"),

View file

@ -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>

View file

@ -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"),

View file

@ -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 }
</>;
}

View file

@ -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>

View file

@ -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>;

View file

@ -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;

View file

@ -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, {

View file

@ -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>;
}
}

View file

@ -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>
);
}
}

View file

@ -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>;
}
}

View file

@ -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>
);
}
}

View file

@ -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}
/>
);
}
}

View file

@ -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>;
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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>
);
}
}

View file

@ -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>
);
}

View file

@ -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">

View file

@ -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}

View file

@ -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']}
/>
</>;
}
}

View file

@ -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

View file

@ -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} />

View file

@ -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>;
}
}

View file

@ -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}
/>;
}
}

View file

@ -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,

View file

@ -146,7 +146,6 @@ export default class ReplyTile extends React.PureComponent<IProps> {
if (needsSenderProfile) {
sender = <SenderProfile
mxEvent={mxEvent}
enableFlair={false}
/>;
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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;

View file

@ -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">

View file

@ -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)}

View file

@ -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} />

View file

@ -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>

View file

@ -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>
);
}
}

View file

@ -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.",
) }

View file

@ -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">

View file

@ -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>