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
171 changed files with 317 additions and 12160 deletions

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