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:
@@ -1,759 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { AddressType, addressTypes, getAddressType, IUserAddress } from '../../../UserAddress';
|
||||
import GroupStore from '../../../stores/GroupStore';
|
||||
import * as Email from '../../../email';
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
|
||||
import { abbreviateUrl } from '../../../utils/UrlUtils';
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import AddressSelector from '../elements/AddressSelector';
|
||||
import AddressTile from '../elements/AddressTile';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
|
||||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
const addressTypeName = {
|
||||
'mx-user-id': _td("Matrix ID"),
|
||||
'mx-room-id': _td("Matrix Room ID"),
|
||||
'email': _td("email address"),
|
||||
};
|
||||
|
||||
interface IResult {
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
room_id?: string; // eslint-disable-line camelcase
|
||||
name?: string;
|
||||
display_name?: string; // eslint-disable-line camelcase
|
||||
avatar_url?: string;// eslint-disable-line camelcase
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
description?: JSX.Element;
|
||||
// Extra node inserted after picker input, dropdown and errors
|
||||
extraNode?: JSX.Element;
|
||||
value?: string;
|
||||
placeholder?: ((validAddressTypes: any) => string) | string;
|
||||
roomId?: string;
|
||||
button?: string;
|
||||
focus?: boolean;
|
||||
validAddressTypes?: AddressType[];
|
||||
onFinished: (success: boolean, list?: IUserAddress[]) => void;
|
||||
groupId?: string;
|
||||
// The type of entity to search for. Default: 'user'.
|
||||
pickerType?: 'user' | 'room';
|
||||
// Whether the current user should be included in the addresses returned. Only
|
||||
// applicable when pickerType is `user`. Default: false.
|
||||
includeSelf?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
// Whether to show an error message because of an invalid address
|
||||
invalidAddressError: boolean;
|
||||
// List of UserAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
selectedList: IUserAddress[];
|
||||
// Whether a search is ongoing
|
||||
busy: boolean;
|
||||
// An error message generated during the user directory search
|
||||
searchError: string;
|
||||
// Whether the server supports the user_directory API
|
||||
serverSupportsUserDirectory: boolean;
|
||||
// The query being searched for
|
||||
query: string;
|
||||
// List of UserAddressType objects representing the set of
|
||||
// auto-completion results for the current search query.
|
||||
suggestedList: IUserAddress[];
|
||||
// List of address types initialised from props, but may change while the
|
||||
// dialog is open and represents the supported list of address types at this time.
|
||||
validAddressTypes: AddressType[];
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.AddressPickerDialog")
|
||||
export default class AddressPickerDialog extends React.Component<IProps, IState> {
|
||||
private textinput = createRef<HTMLTextAreaElement>();
|
||||
private addressSelector = createRef<AddressSelector>();
|
||||
private queryChangedDebouncer: number;
|
||||
private cancelThreepidLookup: () => void;
|
||||
|
||||
static defaultProps: Partial<IProps> = {
|
||||
value: "",
|
||||
focus: true,
|
||||
validAddressTypes: addressTypes,
|
||||
pickerType: 'user',
|
||||
includeSelf: false,
|
||||
};
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
let validAddressTypes = this.props.validAddressTypes;
|
||||
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
|
||||
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes(AddressType.Email)) {
|
||||
validAddressTypes = validAddressTypes.filter(type => type !== AddressType.Email);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
invalidAddressError: false,
|
||||
selectedList: [],
|
||||
busy: false,
|
||||
searchError: null,
|
||||
serverSupportsUserDirectory: true,
|
||||
query: "",
|
||||
suggestedList: [],
|
||||
validAddressTypes,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.focus) {
|
||||
// Set the cursor at the end of the text input
|
||||
this.textinput.current.value = this.props.value;
|
||||
}
|
||||
}
|
||||
|
||||
private getPlaceholder(): string {
|
||||
const { placeholder } = this.props;
|
||||
if (typeof placeholder === "string") {
|
||||
return placeholder;
|
||||
}
|
||||
// Otherwise it's a function, as checked by prop types.
|
||||
return placeholder(this.state.validAddressTypes);
|
||||
}
|
||||
|
||||
private onButtonClick = (): void => {
|
||||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.textinput.current.value !== '') {
|
||||
selectedList = this.addAddressesToList([this.textinput.current.value]);
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, selectedList);
|
||||
};
|
||||
|
||||
private onCancel = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onKeyDown = (e: React.KeyboardEvent): void => {
|
||||
const textInput = this.textinput.current ? this.textinput.current.value : undefined;
|
||||
let handled = true;
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
||||
|
||||
if (action === KeyBindingAction.Escape) {
|
||||
this.props.onFinished(false);
|
||||
} else if (e.key === KeyBindingAction.ArrowUp) {
|
||||
this.addressSelector.current?.moveSelectionUp();
|
||||
} else if (e.key === KeyBindingAction.ArrowDown) {
|
||||
this.addressSelector.current?.moveSelectionDown();
|
||||
} else if (
|
||||
[KeyBindingAction.Comma, KeyBindingAction.Enter, KeyBindingAction.Tab].includes(action) &&
|
||||
this.state.suggestedList.length > 0
|
||||
) {
|
||||
this.addressSelector.current?.chooseSelection();
|
||||
} else if (textInput.length === 0 && this.state.selectedList.length && action === KeyBindingAction.Backspace) {
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.key === KeyBindingAction.Enter) {
|
||||
if (textInput === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
this.addAddressesToList([textInput]);
|
||||
}
|
||||
} else if (textInput && [KeyBindingAction.Comma, KeyBindingAction.Tab].includes(action)) {
|
||||
this.addAddressesToList([textInput]);
|
||||
} else {
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
private onQueryChanged = (ev: React.ChangeEvent): void => {
|
||||
const query = (ev.target as HTMLTextAreaElement).value;
|
||||
if (this.queryChangedDebouncer) {
|
||||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query !== '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.props.pickerType === 'user') {
|
||||
if (this.props.groupId) {
|
||||
this.doNaiveGroupSearch(query);
|
||||
} else if (this.state.serverSupportsUserDirectory) {
|
||||
this.doUserDirectorySearch(query);
|
||||
} else {
|
||||
this.doLocalSearch(query);
|
||||
}
|
||||
} else if (this.props.pickerType === 'room') {
|
||||
if (this.props.groupId) {
|
||||
this.doNaiveGroupRoomSearch(query);
|
||||
} else {
|
||||
this.doRoomSearch(query);
|
||||
}
|
||||
} else {
|
||||
logger.error('Unknown pickerType', this.props.pickerType);
|
||||
}
|
||||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
searchError: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onDismissed = (index: number) => () => {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.splice(index, 1);
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||
};
|
||||
|
||||
private onSelected = (index: number): void => {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(this.getFilteredSuggestions()[index]);
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||
};
|
||||
|
||||
private doNaiveGroupSearch(query: string): void {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().getGroupUsers(this.props.groupId).then((resp) => {
|
||||
const results = [];
|
||||
resp.chunk.forEach((u) => {
|
||||
const userIdMatch = u.user_id.toLowerCase().includes(lowerCaseQuery);
|
||||
const displayNameMatch = (u.displayname || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(userIdMatch || displayNameMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
user_id: u.user_id,
|
||||
avatar_url: u.avatar_url,
|
||||
display_name: u.displayname,
|
||||
});
|
||||
});
|
||||
this.processResults(results, query);
|
||||
}).catch((err) => {
|
||||
logger.error('Error whilst searching group rooms: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doNaiveGroupRoomSearch(query: string): void {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const results = [];
|
||||
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
|
||||
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
|
||||
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
|
||||
if (!(nameMatch || topicMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
results.push({
|
||||
room_id: r.room_id,
|
||||
avatar_url: r.avatar_url,
|
||||
name: r.name || r.canonical_alias,
|
||||
});
|
||||
});
|
||||
this.processResults(results, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
|
||||
private doRoomSearch(query: string): void {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
const results = [];
|
||||
rooms.forEach((room) => {
|
||||
let rank = Infinity;
|
||||
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
|
||||
const name = nameEvent ? nameEvent.getContent().name : '';
|
||||
const canonicalAlias = room.getCanonicalAlias();
|
||||
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
|
||||
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
|
||||
return a.concat(b);
|
||||
}, []);
|
||||
|
||||
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
|
||||
let aliasMatch = false;
|
||||
let shortestMatchingAliasLength = Infinity;
|
||||
aliases.forEach((alias) => {
|
||||
if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
|
||||
aliasMatch = true;
|
||||
if (shortestMatchingAliasLength > alias.length) {
|
||||
shortestMatchingAliasLength = alias.length;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!(nameMatch || aliasMatch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aliasMatch) {
|
||||
// A shorter matching alias will give a better rank
|
||||
rank = shortestMatchingAliasLength;
|
||||
}
|
||||
|
||||
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
|
||||
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
|
||||
|
||||
results.push({
|
||||
rank,
|
||||
room_id: room.roomId,
|
||||
avatar_url: avatarUrl,
|
||||
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by rank ascending (a high rank being less relevant)
|
||||
const sortedResults = results.sort((a, b) => {
|
||||
return a.rank - b.rank;
|
||||
});
|
||||
|
||||
this.processResults(sortedResults, query);
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
|
||||
private doUserDirectorySearch(query: string): void {
|
||||
this.setState({
|
||||
busy: true,
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
MatrixClientPeg.get().searchUserDirectory({
|
||||
term: query,
|
||||
}).then((resp) => {
|
||||
// The query might have changed since we sent the request, so ignore
|
||||
// responses for anything other than the latest query.
|
||||
if (this.state.query !== query) {
|
||||
return;
|
||||
}
|
||||
this.processResults(resp.results, query);
|
||||
}).catch((err) => {
|
||||
logger.error('Error whilst searching user directory: ', err);
|
||||
this.setState({
|
||||
searchError: err.errcode ? err.message : _t('Something went wrong!'),
|
||||
});
|
||||
if (err.errcode === 'M_UNRECOGNIZED') {
|
||||
this.setState({
|
||||
serverSupportsUserDirectory: false,
|
||||
});
|
||||
// Do a local search immediately
|
||||
this.doLocalSearch(query);
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doLocalSearch(query: string): void {
|
||||
this.setState({
|
||||
query,
|
||||
searchError: null,
|
||||
});
|
||||
const queryLowercase = query.toLowerCase();
|
||||
const results = [];
|
||||
MatrixClientPeg.get().getUsers().forEach((user) => {
|
||||
if (user.userId.toLowerCase().indexOf(queryLowercase) === -1 &&
|
||||
user.displayName.toLowerCase().indexOf(queryLowercase) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Put results in the format of the new API
|
||||
results.push({
|
||||
user_id: user.userId,
|
||||
display_name: user.displayName,
|
||||
avatar_url: user.avatarUrl,
|
||||
});
|
||||
});
|
||||
this.processResults(results, query);
|
||||
}
|
||||
|
||||
private processResults(results: IResult[], query: string): void {
|
||||
const suggestedList = [];
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const room = client.getRoom(result.room_id);
|
||||
if (room) {
|
||||
const tombstone = room.currentState.getStateEvents('m.room.tombstone', '');
|
||||
if (tombstone && tombstone.getContent() && tombstone.getContent()["replacement_room"]) {
|
||||
const replacementRoom = client.getRoom(tombstone.getContent()["replacement_room"]);
|
||||
|
||||
// Skip rooms with tombstones where we are also aware of the replacement room.
|
||||
if (replacementRoom) return;
|
||||
}
|
||||
}
|
||||
suggestedList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this.props.includeSelf &&
|
||||
result.user_id === MatrixClientPeg.get().credentials.userId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
suggestedList.push({
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
avatarMxc: result.avatar_url,
|
||||
isKnown: true,
|
||||
});
|
||||
});
|
||||
|
||||
// If the query is a valid address, add an entry for that
|
||||
// This is important, otherwise there's no way to invite
|
||||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (this.state.validAddressTypes.includes(addrType)) {
|
||||
if (addrType === 'email' && !Email.looksValid(query)) {
|
||||
this.setState({ searchError: _t("That doesn't look like a valid email address") });
|
||||
return;
|
||||
}
|
||||
suggestedList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||
if (addrType === 'email') {
|
||||
this.lookupThreepid(addrType, query);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
suggestedList,
|
||||
invalidAddressError: false,
|
||||
}, () => {
|
||||
if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop();
|
||||
});
|
||||
}
|
||||
|
||||
private addAddressesToList(addressTexts: string[]): IUserAddress[] {
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
|
||||
let hasError = false;
|
||||
addressTexts.forEach((addressText) => {
|
||||
addressText = addressText.trim();
|
||||
const addrType = getAddressType(addressText);
|
||||
const addrObj: IUserAddress = {
|
||||
addressType: addrType,
|
||||
address: addressText,
|
||||
isKnown: false,
|
||||
};
|
||||
|
||||
if (!this.state.validAddressTypes.includes(addrType)) {
|
||||
hasError = true;
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType === 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
}
|
||||
|
||||
selectedList.push(addrObj);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
invalidAddressError: hasError ? true : this.state.invalidAddressError,
|
||||
});
|
||||
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
|
||||
return hasError ? null : selectedList;
|
||||
}
|
||||
|
||||
private async lookupThreepid(medium: AddressType, address: string): Promise<string> {
|
||||
let cancelled = false;
|
||||
// Note that we can't safely remove this after we're done
|
||||
// because we don't know that it's the same one, so we just
|
||||
// leave it: it's replacing the old one each time so it's
|
||||
// not like they leak.
|
||||
this.cancelThreepidLookup = function() {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
// wait a bit to let the user finish typing
|
||||
await sleep(500);
|
||||
if (cancelled) return null;
|
||||
|
||||
try {
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = await authClient.getAccessToken();
|
||||
if (cancelled) return null;
|
||||
|
||||
const lookup = await MatrixClientPeg.get().lookupThreePid(
|
||||
medium,
|
||||
address,
|
||||
undefined /* callback */,
|
||||
identityAccessToken,
|
||||
);
|
||||
if (cancelled || lookup === null || !lookup.mxid) return null;
|
||||
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
|
||||
if (cancelled || profile === null) return null;
|
||||
|
||||
this.setState({
|
||||
suggestedList: [{
|
||||
// a UserAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
displayName: profile.displayname,
|
||||
avatarMxc: profile.avatar_url,
|
||||
isKnown: true,
|
||||
}],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
this.setState({
|
||||
searchError: _t('Something went wrong!'),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getFilteredSuggestions(): IUserAddress[] {
|
||||
// map addressType => set of addresses to avoid O(n*m) operation
|
||||
const selectedAddresses = {};
|
||||
this.state.selectedList.forEach(({ address, addressType }) => {
|
||||
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
|
||||
selectedAddresses[addressType].add(address);
|
||||
});
|
||||
|
||||
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||
return this.state.suggestedList.filter(({ address, addressType }) => {
|
||||
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||
});
|
||||
}
|
||||
|
||||
private onPaste = (e: React.ClipboardEvent): void => {
|
||||
// Prevent the text being pasted into the textarea
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData.getData("text");
|
||||
// Process it as a list of addresses to add instead
|
||||
this.addAddressesToList(text.split(/[\s,]+/));
|
||||
};
|
||||
|
||||
private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
|
||||
// Update the IS in account data. Actually using it may trigger terms.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useDefaultIdentityServer();
|
||||
|
||||
// Add email as a valid address type.
|
||||
const { validAddressTypes } = this.state;
|
||||
validAddressTypes.push(AddressType.Email);
|
||||
this.setState({ validAddressTypes });
|
||||
};
|
||||
|
||||
private onManageSettingsClick = (e: React.MouseEvent): void => {
|
||||
e.preventDefault();
|
||||
dis.fire(Action.ViewUserSettings);
|
||||
this.onCancel();
|
||||
};
|
||||
|
||||
render() {
|
||||
let inputLabel;
|
||||
if (this.props.description) {
|
||||
inputLabel = <div className="mx_AddressPickerDialog_label">
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const query = [];
|
||||
// create the invite list
|
||||
if (this.state.selectedList.length > 0) {
|
||||
for (let i = 0; i < this.state.selectedList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.selectedList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the query at the end
|
||||
query.push(
|
||||
<textarea
|
||||
key={this.state.selectedList.length}
|
||||
onPaste={this.onPaste}
|
||||
rows={1}
|
||||
id="textinput"
|
||||
ref={this.textinput}
|
||||
className="mx_AddressPickerDialog_input"
|
||||
onChange={this.onQueryChanged}
|
||||
placeholder={this.getPlaceholder()}
|
||||
defaultValue={this.props.value}
|
||||
autoFocus={this.props.focus}
|
||||
/>,
|
||||
);
|
||||
|
||||
const filteredSuggestedList = this.getFilteredSuggestions();
|
||||
|
||||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.invalidAddressError) {
|
||||
const validTypeDescriptions = this.state.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||
error = <div className="mx_AddressPickerDialog_error">
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
}) }
|
||||
</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_AddressPickerDialog_error">{ this.state.searchError }</div>;
|
||||
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
|
||||
error = <div className="mx_AddressPickerDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={this.addressSelector}
|
||||
addressList={filteredSuggestedList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let identityServer;
|
||||
// If picker cannot currently accept e-mail but should be able to
|
||||
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes(AddressType.Email)
|
||||
&& this.props.validAddressTypes.includes(AddressType.Email)) {
|
||||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||
if (defaultIdentityServerUrl) {
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
{
|
||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||
},
|
||||
{
|
||||
default: sub => (
|
||||
<AccessibleButton kind="link_inline" onClick={this.onUseDefaultIdentityServerClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>
|
||||
),
|
||||
settings: sub => <AccessibleButton kind="link_inline" onClick={this.onManageSettingsClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
) }</div>;
|
||||
} else {
|
||||
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
settings: sub => <AccessibleButton kind="link_inline" onClick={this.onManageSettingsClick}>
|
||||
{ sub }
|
||||
</AccessibleButton>,
|
||||
},
|
||||
) }</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_AddressPickerDialog"
|
||||
onKeyDown={this.onKeyDown}
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
>
|
||||
{ inputLabel }
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
|
||||
{ error }
|
||||
{ addressSelector }
|
||||
{ this.props.extraNode }
|
||||
{ identityServer }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onButtonClick}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
|
||||
<p>
|
||||
{ _t(
|
||||
"Debug logs contain application usage data including your " +
|
||||
"username, the IDs or aliases of the rooms or groups you " +
|
||||
"username, the IDs or aliases of the rooms you " +
|
||||
"have visited, which UI elements you last interacted with, " +
|
||||
"and the usernames of other users. They do not contain messages.",
|
||||
) }
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, FormEvent } from 'react';
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { arrayFastClone } from "../../../utils/arrays";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import InviteDialog from "./InviteDialog";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import { inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
import Modal from "../../../Modal";
|
||||
import ErrorDialog from "./ErrorDialog";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
roomId: string;
|
||||
communityName: string;
|
||||
}
|
||||
|
||||
interface IPerson {
|
||||
userId: string;
|
||||
user: RoomMember;
|
||||
lastActive: number;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
emailTargets: string[];
|
||||
userTargets: string[];
|
||||
showPeople: boolean;
|
||||
people: IPerson[];
|
||||
numPeople: number;
|
||||
busy: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CommunityPrototypeInviteDialog")
|
||||
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
emailTargets: [],
|
||||
userTargets: [],
|
||||
showPeople: false,
|
||||
people: this.buildSuggestions(),
|
||||
numPeople: 5, // arbitrary default
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
private buildSuggestions(): IPerson[] {
|
||||
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get("welcome_user_id")]);
|
||||
if (this.props.roomId) {
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
|
||||
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
|
||||
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
|
||||
// add banned users, so we don't try to invite them
|
||||
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
|
||||
}
|
||||
|
||||
return InviteDialog.buildRecents(alreadyInvited);
|
||||
}
|
||||
|
||||
private onSubmit = async (ev: FormEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
const targets = [...this.state.emailTargets, ...this.state.userTargets];
|
||||
const result = await inviteMultipleToRoom(this.props.roomId, targets);
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
const success = showAnyInviteErrors(result.states, room, result.inviter);
|
||||
if (success) {
|
||||
this.props.onFinished(true);
|
||||
} else {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
} catch (e) {
|
||||
this.setState({ busy: false });
|
||||
logger.error(e);
|
||||
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
|
||||
title: _t("Failed to invite"),
|
||||
description: ((e && e.message) ? e.message : _t("Operation failed")),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAddressChange = (ev: ChangeEvent<HTMLInputElement>, index: number) => {
|
||||
const targets = arrayFastClone(this.state.emailTargets);
|
||||
if (index >= targets.length) {
|
||||
targets.push(ev.target.value);
|
||||
} else {
|
||||
targets[index] = ev.target.value;
|
||||
}
|
||||
this.setState({ emailTargets: targets });
|
||||
};
|
||||
|
||||
private onAddressBlur = (index: number) => {
|
||||
const targets = arrayFastClone(this.state.emailTargets);
|
||||
if (index >= targets.length) return; // not important
|
||||
if (targets[index].trim() === "") {
|
||||
targets.splice(index, 1);
|
||||
this.setState({ emailTargets: targets });
|
||||
}
|
||||
};
|
||||
|
||||
private onShowPeopleClick = () => {
|
||||
this.setState({ showPeople: !this.state.showPeople });
|
||||
};
|
||||
|
||||
private setPersonToggle = (person: IPerson, selected: boolean) => {
|
||||
const targets = arrayFastClone(this.state.userTargets);
|
||||
if (selected && !targets.includes(person.userId)) {
|
||||
targets.push(person.userId);
|
||||
} else if (!selected && targets.includes(person.userId)) {
|
||||
targets.splice(targets.indexOf(person.userId), 1);
|
||||
}
|
||||
this.setState({ userTargets: targets });
|
||||
};
|
||||
|
||||
private renderPerson(person: IPerson, key: any) {
|
||||
const avatarSize = 36;
|
||||
let avatarUrl = null;
|
||||
if (person.user.getMxcAvatarUrl()) {
|
||||
avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize);
|
||||
}
|
||||
return (
|
||||
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
|
||||
<BaseAvatar
|
||||
url={avatarUrl}
|
||||
name={person.user.name}
|
||||
idName={person.user.userId}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
/>
|
||||
<div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
|
||||
<span className="mx_CommunityPrototypeInviteDialog_personName">{ person.user.name }</span>
|
||||
<span className="mx_CommunityPrototypeInviteDialog_personId">{ person.userId }</span>
|
||||
</div>
|
||||
<StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onShowMorePeople = () => {
|
||||
this.setState({ numPeople: this.state.numPeople + 5 }); // arbitrary increase
|
||||
};
|
||||
|
||||
public render() {
|
||||
const emailAddresses = [];
|
||||
this.state.emailTargets.forEach((address, i) => {
|
||||
emailAddresses.push((
|
||||
<Field
|
||||
key={i}
|
||||
value={address}
|
||||
onChange={(e) => this.onAddressChange(e, i)}
|
||||
label={_t("Email address")}
|
||||
placeholder={_t("Email address")}
|
||||
onBlur={() => this.onAddressBlur(i)}
|
||||
/>
|
||||
));
|
||||
});
|
||||
|
||||
// Push a clean input
|
||||
emailAddresses.push((
|
||||
<Field
|
||||
key={emailAddresses.length}
|
||||
value=""
|
||||
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
|
||||
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
||||
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
|
||||
/>
|
||||
));
|
||||
|
||||
let peopleIntro = null;
|
||||
const people = [];
|
||||
if (this.state.showPeople) {
|
||||
const humansToPresent = this.state.people.slice(0, this.state.numPeople);
|
||||
humansToPresent.forEach((person, i) => {
|
||||
people.push(this.renderPerson(person, i));
|
||||
});
|
||||
if (humansToPresent.length < this.state.people.length) {
|
||||
people.push((
|
||||
<AccessibleButton
|
||||
onClick={this.onShowMorePeople}
|
||||
kind="link"
|
||||
key="more"
|
||||
className="mx_CommunityPrototypeInviteDialog_morePeople"
|
||||
>
|
||||
{ _t("Show more") }
|
||||
</AccessibleButton>
|
||||
));
|
||||
}
|
||||
}
|
||||
if (this.state.people.length > 0) {
|
||||
peopleIntro = (
|
||||
<div className="mx_CommunityPrototypeInviteDialog_people">
|
||||
<span>{ _t("People you know on %(brand)s", { brand: SdkConfig.get().brand }) }</span>
|
||||
<AccessibleButton onClick={this.onShowPeopleClick}>
|
||||
{ this.state.showPeople ? _t("Hide") : _t("Show") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let buttonText = _t("Skip");
|
||||
const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
|
||||
if (targetCount > 0) {
|
||||
buttonText = _t("Send %(count)s invites", { count: targetCount });
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_CommunityPrototypeInviteDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Invite people to join %(communityName)s", { communityName: this.props.communityName })}
|
||||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
{ emailAddresses }
|
||||
{ peopleIntro }
|
||||
{ people }
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onSubmit}
|
||||
disabled={this.state.busy}
|
||||
className="mx_CommunityPrototypeInviteDialog_primaryButton"
|
||||
>
|
||||
{ buttonText }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
|
||||
|
||||
type BaseProps = ComponentProps<typeof ConfirmUserActionDialog>;
|
||||
interface IProps extends Omit<BaseProps, "groupMember" | "matrixClient" | "children" | "onFinished"> {
|
||||
interface IProps extends Omit<BaseProps, "matrixClient" | "children" | "onFinished"> {
|
||||
space: Room;
|
||||
allLabel: string;
|
||||
specificLabel: string;
|
||||
|
||||
@@ -15,28 +15,20 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, FormEvent, ReactNode } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
import Field from '../elements/Field';
|
||||
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
||||
|
||||
interface IProps {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member?: RoomMember;
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember?: GroupMemberType;
|
||||
// needed if a group member is specified
|
||||
matrixClient?: MatrixClient;
|
||||
// matrix-js-sdk (room) member object.
|
||||
member: RoomMember;
|
||||
action: string; // eg. 'Ban'
|
||||
title: string; // eg. 'Ban this user?'
|
||||
|
||||
@@ -112,21 +104,9 @@ export default class ConfirmUserActionDialog extends React.Component<IProps, ISt
|
||||
);
|
||||
}
|
||||
|
||||
let avatar;
|
||||
let name;
|
||||
let userId;
|
||||
if (this.props.member) {
|
||||
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||
name = this.props.member.name;
|
||||
userId = this.props.member.userId;
|
||||
} else {
|
||||
const httpAvatarUrl = this.props.groupMember.avatarUrl
|
||||
? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48)
|
||||
: null;
|
||||
name = this.props.groupMember.displayname || this.props.groupMember.userId;
|
||||
userId = this.props.groupMember.userId;
|
||||
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
|
||||
}
|
||||
const avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
|
||||
const name = this.props.member.name;
|
||||
const userId = this.props.member.userId;
|
||||
|
||||
const displayUserIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(
|
||||
userId, { roomId: this.props.roomId, withDisplayName: true },
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import InfoTooltip from "../elements/InfoTooltip";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { showCommunityRoomInviteDialog } from "../../../RoomInvite";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
}
|
||||
|
||||
interface IState {
|
||||
name: string;
|
||||
localpart: string;
|
||||
error: string;
|
||||
busy: boolean;
|
||||
avatarFile: File;
|
||||
avatarPreview: string;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateCommunityPrototypeDialog")
|
||||
export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
|
||||
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
name: "",
|
||||
localpart: "",
|
||||
error: null,
|
||||
busy: false,
|
||||
avatarFile: null,
|
||||
avatarPreview: null,
|
||||
};
|
||||
}
|
||||
|
||||
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
|
||||
this.setState({ name: ev.target.value, localpart });
|
||||
};
|
||||
|
||||
private onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.state.busy) return;
|
||||
|
||||
// We'll create the community now to see if it's taken, leaving it active in
|
||||
// the background for the user to look at while they invite people.
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
let avatarUrl = ''; // must be a string for synapse to accept it
|
||||
if (this.state.avatarFile) {
|
||||
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
|
||||
}
|
||||
|
||||
const result = await MatrixClientPeg.get().createGroup({
|
||||
localpart: this.state.localpart,
|
||||
profile: {
|
||||
name: this.state.name,
|
||||
avatar_url: avatarUrl,
|
||||
},
|
||||
});
|
||||
|
||||
// Ensure the tag gets selected now that we've created it
|
||||
dis.dispatch({ action: 'deselect_tags' }, true);
|
||||
dis.dispatch({
|
||||
action: 'select_tag',
|
||||
tag: result.group_id,
|
||||
});
|
||||
|
||||
// Close our own dialog before moving much further
|
||||
this.props.onFinished(true);
|
||||
|
||||
if (result.room_id) {
|
||||
// Force the group store to update as it might have missed the general chat
|
||||
await GroupStore.refreshGroupRooms(result.group_id);
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: result.room_id,
|
||||
metricsTrigger: undefined, // Deprecated groups
|
||||
});
|
||||
showCommunityRoomInviteDialog(result.room_id, this.state.name);
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: result.group_id,
|
||||
group_is_new: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
this.setState({
|
||||
busy: false,
|
||||
error: _t(
|
||||
"There was an error creating your community. The name may be taken or the " +
|
||||
"server is unable to process your request.",
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || !e.target.files.length) {
|
||||
this.setState({ avatarFile: null });
|
||||
} else {
|
||||
this.setState({ busy: true });
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev: ProgressEvent<FileReader>) => {
|
||||
this.setState({ avatarFile: file, busy: false, avatarPreview: ev.target.result as string });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
private onChangeAvatar = () => {
|
||||
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
|
||||
};
|
||||
|
||||
public render() {
|
||||
let communityId = null;
|
||||
if (this.state.localpart) {
|
||||
communityId = (
|
||||
<span className="mx_CreateCommunityPrototypeDialog_communityId">
|
||||
{ _t("Community ID: +<localpart />:%(domain)s", {
|
||||
domain: MatrixClientPeg.getHomeserverName(),
|
||||
}, {
|
||||
localpart: () => <u>{ this.state.localpart }</u>,
|
||||
}) }
|
||||
<InfoTooltip
|
||||
tooltip={_t(
|
||||
"Use this when referencing your community to others. The community ID " +
|
||||
"cannot be changed.",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let helpText = (
|
||||
<span className="mx_CreateCommunityPrototypeDialog_subtext">
|
||||
{ _t("You can change this later if needed.") }
|
||||
</span>
|
||||
);
|
||||
if (this.state.error) {
|
||||
const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error";
|
||||
helpText = (
|
||||
<span className={classes}>
|
||||
{ this.state.error }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
|
||||
if (!this.state.avatarPreview) {
|
||||
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_CreateCommunityPrototypeDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("What's the name of your community or team?")}
|
||||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateCommunityPrototypeDialog_colName">
|
||||
<Field
|
||||
value={this.state.name}
|
||||
onChange={this.onNameChange}
|
||||
placeholder={_t("Enter name")}
|
||||
label={_t("Enter name")}
|
||||
/>
|
||||
{ helpText }
|
||||
<span className="mx_CreateCommunityPrototypeDialog_subtext">
|
||||
{ /*nbsp is to reserve the height of this element when there's nothing*/ }
|
||||
{ communityId }
|
||||
</span>
|
||||
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
|
||||
{ _t("Create") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
|
||||
<input
|
||||
type="file"
|
||||
style={{ display: "none" }}
|
||||
ref={this.avatarUploadRef}
|
||||
accept="image/*"
|
||||
onChange={this.onAvatarChanged}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onChangeAvatar}
|
||||
className="mx_CreateCommunityPrototypeDialog_avatarContainer"
|
||||
>
|
||||
{ preview }
|
||||
</AccessibleButton>
|
||||
<div className="mx_CreateCommunityPrototypeDialog_tip">
|
||||
<b>{ _t("Add image (optional)") }</b>
|
||||
<span>
|
||||
{ _t("An image will help people identify your community.") }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
onFinished: (success: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
groupName: string;
|
||||
groupId: string;
|
||||
groupIdError: string;
|
||||
creating: boolean;
|
||||
createError: Error;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.CreateGroupDialog")
|
||||
export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||
public state = {
|
||||
groupName: '',
|
||||
groupId: '',
|
||||
groupIdError: '',
|
||||
creating: false,
|
||||
createError: null,
|
||||
};
|
||||
|
||||
private onGroupNameChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
groupName: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onGroupIdChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||
this.setState({
|
||||
groupId: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
private onGroupIdBlur = (): void => {
|
||||
this.checkGroupId();
|
||||
};
|
||||
|
||||
private checkGroupId() {
|
||||
let error = null;
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
} else if (!/^[a-z0-9=_\-./]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
// Reset createError to get rid of now stale error message
|
||||
createError: null,
|
||||
});
|
||||
return error;
|
||||
}
|
||||
|
||||
private onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.checkGroupId()) return;
|
||||
|
||||
const profile: any = {};
|
||||
if (this.state.groupName !== '') {
|
||||
profile.name = this.state.groupName;
|
||||
}
|
||||
this.setState({ creating: true });
|
||||
MatrixClientPeg.get().createGroup({
|
||||
localpart: this.state.groupId,
|
||||
profile: profile,
|
||||
}).then((result) => {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: result.group_id,
|
||||
group_is_new: true,
|
||||
});
|
||||
this.props.onFinished(true);
|
||||
}).catch((e) => {
|
||||
this.setState({ createError: e });
|
||||
}).finally(() => {
|
||||
this.setState({ creating: false });
|
||||
});
|
||||
};
|
||||
|
||||
private onCancel = () => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.creating) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let createErrorNode;
|
||||
if (this.state.createError) {
|
||||
// XXX: We should catch errcodes and give sensible i18ned messages for them,
|
||||
// rather than displaying what the server gives us, but synapse doesn't give
|
||||
// any yet.
|
||||
createErrorNode = <div className="error" role="alert">
|
||||
<div>{ _t('Something went wrong whilst creating your community') }</div>
|
||||
<div>{ this.state.createError.message }</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_CreateGroupDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t('Create Community')}
|
||||
>
|
||||
<form onSubmit={this.onFormSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupname">{ _t('Community Name') }</label>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
id="groupname"
|
||||
className="mx_CreateGroupDialog_input"
|
||||
autoFocus={true}
|
||||
size={64}
|
||||
placeholder={_t('Example')}
|
||||
onChange={this.onGroupNameChange}
|
||||
value={this.state.groupName}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_CreateGroupDialog_inputRow">
|
||||
<div className="mx_CreateGroupDialog_label">
|
||||
<label htmlFor="groupid">{ _t('Community ID') }</label>
|
||||
</div>
|
||||
<div className="mx_CreateGroupDialog_input_group">
|
||||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||
<input id="groupid"
|
||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||
size={32}
|
||||
placeholder={_t('example')}
|
||||
onChange={this.onGroupIdChange}
|
||||
onBlur={this.onGroupIdBlur}
|
||||
value={this.state.groupId}
|
||||
/>
|
||||
<span className="mx_CreateGroupDialog_suffix">
|
||||
:{ MatrixClientPeg.get().getDomain() }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error">
|
||||
{ this.state.groupIdError }
|
||||
</div>
|
||||
{ createErrorNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import withValidation, { IFieldState } from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Heading from "../typography/Heading";
|
||||
import Field from "../elements/Field";
|
||||
@@ -122,10 +121,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
createOpts.creation_content = { 'm.federate': false };
|
||||
}
|
||||
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
}
|
||||
|
||||
opts.parentSpace = this.props.parentSpace;
|
||||
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
|
||||
opts.joinRule = JoinRule.Restricted;
|
||||
@@ -250,14 +245,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
let publicPrivateLabel: JSX.Element;
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
publicPrivateLabel = <p>
|
||||
{ _t(
|
||||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
||||
"found and joined by anyone in this community.",
|
||||
) }
|
||||
</p>;
|
||||
} else if (this.state.joinRule === JoinRule.Restricted) {
|
||||
if (this.state.joinRule === JoinRule.Restricted) {
|
||||
publicPrivateLabel = <p>
|
||||
{ _t(
|
||||
"Everyone in <SpaceName/> will be able to find and join this room.", {}, {
|
||||
@@ -332,10 +320,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
let title = _t("Create a room");
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
title = _t("Create a room in %(communityName)s", { communityName: name });
|
||||
} else if (!this.props.parentSpace) {
|
||||
if (!this.props.parentSpace) {
|
||||
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
|
||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||
import Field from "../elements/Field";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import { GroupMember } from "../right_panel/UserInfo";
|
||||
import { parseMembersResponse, parseRoomsResponse } from "../../../stores/GroupStore";
|
||||
import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import Modal from "../../../Modal";
|
||||
import InfoDialog from "./InfoDialog";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "./UserSettingsDialog";
|
||||
import TagOrderActions from "../../../actions/TagOrderActions";
|
||||
import { inviteUsersToRoom } from "../../../RoomInvite";
|
||||
import ProgressBar from "../elements/ProgressBar";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { CreateEventField, IGroupRoom, IGroupSummary } from "../../../@types/groups";
|
||||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
groupId: string;
|
||||
onFinished(spaceId?: string): void;
|
||||
}
|
||||
|
||||
enum Progress {
|
||||
NotStarted,
|
||||
ValidatingInputs,
|
||||
FetchingData,
|
||||
CreatingSpace,
|
||||
InvitingUsers,
|
||||
// anything beyond here is inviting user n - 4
|
||||
}
|
||||
|
||||
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>(null);
|
||||
|
||||
const [progress, setProgress] = useState(Progress.NotStarted);
|
||||
const [numInvites, setNumInvites] = useState(0);
|
||||
const busy = progress > 0;
|
||||
|
||||
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const [name, setName] = useState("");
|
||||
const spaceNameField = useRef<Field>();
|
||||
const [alias, setAlias] = useState("#" + groupId.substring(1, groupId.indexOf(":")) + ":" + cli.getDomain());
|
||||
const spaceAliasField = useRef<RoomAliasField>();
|
||||
const [topic, setTopic] = useState("");
|
||||
const [joinRule, setJoinRule] = useState<JoinRule>(JoinRule.Public);
|
||||
|
||||
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [groupId]);
|
||||
useEffect(() => {
|
||||
if (groupSummary) {
|
||||
setName(groupSummary.profile.name || "");
|
||||
setTopic(groupSummary.profile.short_description || "");
|
||||
setJoinRule(groupSummary.profile.is_openly_joinable ? JoinRule.Public : JoinRule.Invite);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [groupSummary]);
|
||||
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const onCreateSpaceClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
|
||||
setError(null);
|
||||
setProgress(Progress.ValidatingInputs);
|
||||
|
||||
// require & validate the space name field
|
||||
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
|
||||
setProgress(0);
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
return;
|
||||
}
|
||||
// validate the space name alias field but do not require it
|
||||
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
|
||||
setProgress(0);
|
||||
spaceAliasField.current.focus();
|
||||
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setProgress(Progress.FetchingData);
|
||||
|
||||
const [rooms, members, invitedMembers] = await Promise.all([
|
||||
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
|
||||
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
|
||||
]);
|
||||
|
||||
setNumInvites(members.length + invitedMembers.length);
|
||||
|
||||
const viaMap = new Map<string, string[]>();
|
||||
for (const { roomId, canonicalAlias } of rooms) {
|
||||
const room = cli.getRoom(roomId);
|
||||
if (room) {
|
||||
viaMap.set(roomId, calculateRoomVia(room));
|
||||
} else if (canonicalAlias) {
|
||||
try {
|
||||
const { servers } = await cli.getRoomIdForAlias(canonicalAlias);
|
||||
viaMap.set(roomId, servers);
|
||||
} catch (e) {
|
||||
logger.warn("Failed to resolve alias during community migration", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!viaMap.get(roomId)?.length) {
|
||||
// XXX: lets guess the via, this might end up being incorrect.
|
||||
const str = canonicalAlias || roomId;
|
||||
viaMap.set(roomId, [str.substring(1, str.indexOf(":"))]);
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(Progress.CreatingSpace);
|
||||
|
||||
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
|
||||
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
|
||||
creation_content: {
|
||||
[CreateEventField]: groupId,
|
||||
},
|
||||
initial_state: rooms.map(({ roomId }) => ({
|
||||
type: EventType.SpaceChild,
|
||||
state_key: roomId,
|
||||
content: {
|
||||
via: viaMap.get(roomId) || [],
|
||||
},
|
||||
})),
|
||||
// we do not specify the inviters here because Synapse applies a limit and this may cause it to trip
|
||||
}, {
|
||||
andView: false,
|
||||
});
|
||||
|
||||
setProgress(Progress.InvitingUsers);
|
||||
|
||||
const userIds = [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId());
|
||||
await inviteUsersToRoom(roomId, userIds, false, () => setProgress(p => p + 1));
|
||||
|
||||
// eagerly remove it from the community panel
|
||||
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
|
||||
|
||||
// don't bother awaiting this, as we don't hugely care if it fails
|
||||
cli.setGroupProfile(groupId, {
|
||||
...groupSummary.profile,
|
||||
long_description: `<a href="${makeRoomPermalink(roomId)}"><h1>` +
|
||||
_t("This community has been upgraded into a Space") + `</h1></a><br />`
|
||||
+ groupSummary.profile.long_description,
|
||||
} as IGroupSummary["profile"]).catch(e => {
|
||||
logger.warn("Failed to update community profile during migration", e);
|
||||
});
|
||||
|
||||
onFinished(roomId);
|
||||
|
||||
const onSpaceClick = () => {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined, // other
|
||||
});
|
||||
};
|
||||
|
||||
const onPreferencesClick = () => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Preferences,
|
||||
});
|
||||
};
|
||||
|
||||
let spacesDisabledCopy;
|
||||
if (!SpaceStore.spacesEnabled) {
|
||||
spacesDisabledCopy = _t("To view Spaces, hide communities in <a>Preferences</a>", {}, {
|
||||
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("Space created"),
|
||||
description: <>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark" />
|
||||
<p>
|
||||
{ _t("<SpaceName/> has been made and everyone who was a part of the community has " +
|
||||
"been invited to it.", {}, {
|
||||
SpaceName: () => <AccessibleButton onClick={onSpaceClick} kind="link">
|
||||
{ name }
|
||||
</AccessibleButton>,
|
||||
}) }
|
||||
|
||||
{ spacesDisabledCopy }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("To create a Space from another community, just pick the community in Preferences.") }
|
||||
</p>
|
||||
</>,
|
||||
button: _t("Preferences"),
|
||||
onFinished: (openPreferences: boolean) => {
|
||||
if (openPreferences) {
|
||||
onPreferencesClick();
|
||||
}
|
||||
},
|
||||
}, "mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog");
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
setError(e);
|
||||
}
|
||||
|
||||
setProgress(Progress.NotStarted);
|
||||
};
|
||||
|
||||
let footer;
|
||||
if (error) {
|
||||
footer = <>
|
||||
<img src={require("../../../../res/img/element-icons/warning-badge.svg").default} height="24" width="24" alt="" />
|
||||
|
||||
<span className="mx_CreateSpaceFromCommunityDialog_error">
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_errorHeading">{ _t("Failed to migrate community") }</div>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_errorCaption">{ _t("Try again") }</div>
|
||||
</span>
|
||||
|
||||
<AccessibleButton className="mx_CreateSpaceFromCommunityDialog_retryButton" onClick={onCreateSpaceClick}>
|
||||
{ _t("Retry") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
} else if (busy) {
|
||||
let description: string;
|
||||
switch (progress) {
|
||||
case Progress.ValidatingInputs:
|
||||
case Progress.FetchingData:
|
||||
description = _t("Fetching data...");
|
||||
break;
|
||||
case Progress.CreatingSpace:
|
||||
description = _t("Creating Space...");
|
||||
break;
|
||||
case Progress.InvitingUsers:
|
||||
default:
|
||||
description = _t("Adding rooms... (%(progress)s out of %(count)s)", {
|
||||
count: numInvites,
|
||||
progress,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
footer = <span>
|
||||
<ProgressBar
|
||||
value={progress > Progress.FetchingData ? progress : 0}
|
||||
max={numInvites + Progress.InvitingUsers}
|
||||
/>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_progressText">
|
||||
{ description }
|
||||
</div>
|
||||
</span>;
|
||||
} else {
|
||||
footer = <>
|
||||
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
|
||||
{ _t("Create Space") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Create Space from community")}
|
||||
className="mx_CreateSpaceFromCommunityDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_content">
|
||||
<p>
|
||||
{ _t("A link to the Space will be put in your community description.") }
|
||||
|
||||
{ _t("All rooms will be added and all community members will be invited.") }
|
||||
</p>
|
||||
<p className="mx_CreateSpaceFromCommunityDialog_flairNotice">
|
||||
{ _t("Flair won't be available in Spaces for the foreseeable future.") }
|
||||
</p>
|
||||
|
||||
<SpaceCreateForm
|
||||
busy={busy}
|
||||
onSubmit={onCreateSpaceClick}
|
||||
avatarUrl={groupSummary.profile.avatar_url
|
||||
? mediaFromMxc(groupSummary.profile.avatar_url).getThumbnailOfSourceHttp(80, 80, "crop")
|
||||
: undefined
|
||||
}
|
||||
setAvatar={setAvatar}
|
||||
name={name}
|
||||
setName={setName}
|
||||
nameFieldRef={spaceNameField}
|
||||
topic={topic}
|
||||
setTopic={setTopic}
|
||||
alias={alias}
|
||||
setAlias={setAlias}
|
||||
showAliasField={joinRule === JoinRule.Public}
|
||||
aliasFieldRef={spaceAliasField}
|
||||
>
|
||||
<p>{ _t("This description will be shown to people when they view your space") }</p>
|
||||
<JoinRuleDropdown
|
||||
label={_t("Space visibility")}
|
||||
labelInvite={_t("Private space (invite only)")}
|
||||
labelPublic={_t("Public space")}
|
||||
value={joinRule}
|
||||
onChange={setJoinRule}
|
||||
/>
|
||||
<p>{ joinRule === JoinRule.Public
|
||||
? _t("Open space for anyone, best for communities")
|
||||
: _t("Invite only, best for yourself or teams")
|
||||
}</p>
|
||||
{ joinRule !== JoinRule.Public &&
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_nonPublicSpacer" />
|
||||
}
|
||||
</SpaceCreateForm>
|
||||
</div>
|
||||
|
||||
<div className="mx_CreateSpaceFromCommunityDialog_footer">
|
||||
{ footer }
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default CreateSpaceFromCommunityDialog;
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import Field from "../elements/Field";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
communityId: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
name: string;
|
||||
error: string;
|
||||
busy: boolean;
|
||||
currentAvatarUrl: string;
|
||||
avatarFile: File;
|
||||
avatarPreview: string;
|
||||
}
|
||||
|
||||
// XXX: This is a lot of duplication from the create dialog, just in a different shape
|
||||
@replaceableComponent("views.dialogs.EditCommunityPrototypeDialog")
|
||||
export default class EditCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
|
||||
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const profile = CommunityPrototypeStore.instance.getCommunityProfile(props.communityId);
|
||||
|
||||
this.state = {
|
||||
name: profile?.name || "",
|
||||
error: null,
|
||||
busy: false,
|
||||
avatarFile: null,
|
||||
avatarPreview: null,
|
||||
currentAvatarUrl: profile?.avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ name: ev.target.value });
|
||||
};
|
||||
|
||||
private onSubmit = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.state.busy) return;
|
||||
|
||||
// We'll create the community now to see if it's taken, leaving it active in
|
||||
// the background for the user to look at while they invite people.
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
let avatarUrl = this.state.currentAvatarUrl || ""; // must be a string for synapse to accept it
|
||||
if (this.state.avatarFile) {
|
||||
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
|
||||
}
|
||||
|
||||
await MatrixClientPeg.get().setGroupProfile(this.props.communityId, {
|
||||
name: this.state.name,
|
||||
avatar_url: avatarUrl,
|
||||
});
|
||||
|
||||
// ask the flair store to update the profile too
|
||||
await FlairStore.refreshGroupProfile(MatrixClientPeg.get(), this.props.communityId);
|
||||
|
||||
// we did it, so close the dialog
|
||||
this.props.onFinished(true);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
this.setState({
|
||||
busy: false,
|
||||
error: _t("There was an error updating your community. The server is unable to process your request."),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files || !e.target.files.length) {
|
||||
this.setState({ avatarFile: null });
|
||||
} else {
|
||||
this.setState({ busy: true });
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev: ProgressEvent<FileReader>) => {
|
||||
this.setState({ avatarFile: file, busy: false, avatarPreview: ev.target.result as string });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
private onChangeAvatar = () => {
|
||||
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
|
||||
};
|
||||
|
||||
public render() {
|
||||
let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
|
||||
if (!this.state.avatarPreview) {
|
||||
if (this.state.currentAvatarUrl) {
|
||||
const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
|
||||
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
|
||||
} else {
|
||||
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
className="mx_EditCommunityPrototypeDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
title={_t("Update community")}
|
||||
>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_EditCommunityPrototypeDialog_rowName">
|
||||
<Field
|
||||
value={this.state.name}
|
||||
onChange={this.onNameChange}
|
||||
placeholder={_t("Enter name")}
|
||||
label={_t("Enter name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_EditCommunityPrototypeDialog_rowAvatar">
|
||||
<input
|
||||
type="file"
|
||||
style={{ display: "none" }}
|
||||
ref={this.avatarUploadRef}
|
||||
accept="image/*"
|
||||
onChange={this.onAvatarChanged}
|
||||
/>
|
||||
<AccessibleButton
|
||||
onClick={this.onChangeAvatar}
|
||||
className="mx_EditCommunityPrototypeDialog_avatarContainer"
|
||||
>{ preview }</AccessibleButton>
|
||||
<div className="mx_EditCommunityPrototypeDialog_tip">
|
||||
<b>{ _t("Add image (optional)") }</b>
|
||||
<span>
|
||||
{ _t("An image will help people identify your community.") }
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { Layout } from "../../../settings/enums/Layout";
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
@@ -43,7 +42,6 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import EntityTile from "../rooms/EntityTile";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { roomContextDetailsText } from "../../../Rooms";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
@@ -190,16 +188,13 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const spacesEnabled = SpaceStore.spacesEnabled;
|
||||
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
|
||||
const previewLayout = useSettingValue<Layout>("layout");
|
||||
|
||||
let rooms = useMemo(() => sortRooms(
|
||||
cli.getVisibleRooms().filter(
|
||||
room => room.getMyMembership() === "join" &&
|
||||
!(spacesEnabled && room.isSpaceRoom()),
|
||||
room => room.getMyMembership() === "join" && !room.isSpaceRoom(),
|
||||
),
|
||||
), [cli, spacesEnabled]);
|
||||
), [cli]);
|
||||
|
||||
if (lcQuery) {
|
||||
rooms = new QueryMatcher<Room>(rooms, {
|
||||
@@ -241,7 +236,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||
<EventTile
|
||||
mxEvent={mockEvent}
|
||||
layout={previewLayout}
|
||||
enableFlair={flairEnabled}
|
||||
permalinkCreator={permalinkCreator}
|
||||
as="div"
|
||||
/>
|
||||
|
||||
@@ -43,12 +43,10 @@ import {
|
||||
IInviteResult,
|
||||
inviteMultipleToRoom,
|
||||
showAnyInviteErrors,
|
||||
showCommunityInviteDialog,
|
||||
} from "../../../RoomInvite";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
@@ -64,7 +62,6 @@ import QuestionDialog from "./QuestionDialog";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
||||
import CopyableText from "../elements/CopyableText";
|
||||
@@ -1104,23 +1101,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onCommunityInviteClick = (e) => {
|
||||
this.props.onFinished(false);
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
};
|
||||
|
||||
private renderSection(kind: "recents"|"suggestions") {
|
||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||
const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this);
|
||||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||
let sectionSubname = null;
|
||||
|
||||
if (kind === 'suggestions' && CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
sectionSubname = _t("May include members not in %(communityName)s", { communityName });
|
||||
}
|
||||
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
||||
@@ -1199,7 +1185,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{ sectionName }</h3>
|
||||
{ sectionSubname ? <p className="mx_InviteDialog_subname">{ sectionSubname }</p> : null }
|
||||
{ tiles }
|
||||
{ showMore }
|
||||
</div>
|
||||
@@ -1247,7 +1232,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
|
||||
if (defaultIdentityServerUrl) {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
<div className="mx_InviteDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
|
||||
"or manage in <settings>Settings</settings>.",
|
||||
@@ -1268,7 +1253,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="mx_AddressPickerDialog_identityServer">{ _t(
|
||||
<div className="mx_InviteDialog_identityServer">{ _t(
|
||||
"Use an identity server to invite by email. " +
|
||||
"Manage in <settings>Settings</settings>.",
|
||||
{}, {
|
||||
@@ -1377,35 +1362,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
);
|
||||
}
|
||||
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
const inviteText = _t(
|
||||
"This won't invite them to %(communityName)s. " +
|
||||
"To invite someone to %(communityName)s, click <a>here</a>",
|
||||
{ communityName }, {
|
||||
userId: () => {
|
||||
return (
|
||||
<a
|
||||
href={makeUserPermalink(userId)}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>{ userId }</a>
|
||||
);
|
||||
},
|
||||
a: (sub) => {
|
||||
return (
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
onClick={this.onCommunityInviteClick}
|
||||
>{ sub }</AccessibleButton>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
helpText = <React.Fragment>
|
||||
{ helpText } { inviteText }
|
||||
</React.Fragment>;
|
||||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this.startDm;
|
||||
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
|
||||
@@ -1423,7 +1379,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||
</div>;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||
const isSpace = SpaceStore.spacesEnabled && room?.isSpaceRoom();
|
||||
const isSpace = room?.isSpaceRoom();
|
||||
title = isSpace
|
||||
? _t("Invite to %(spaceName)s", {
|
||||
spaceName: room.name || _t("Unnamed Space"),
|
||||
|
||||
@@ -18,13 +18,12 @@ limitations under the License.
|
||||
import * as React from 'react';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { User } from "matrix-js-sdk/src/models/user";
|
||||
import { Group } from "matrix-js-sdk/src/models/group";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from "../elements/QRCode";
|
||||
import { RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import { RoomPermalinkCreator, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
|
||||
import { selectText } from "../../../utils/strings";
|
||||
import StyledCheckbox from '../elements/StyledCheckbox';
|
||||
import { IDialogProps } from "./IDialogProps";
|
||||
@@ -63,7 +62,7 @@ const socials = [
|
||||
];
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
target: Room | User | Group | RoomMember | MatrixEvent;
|
||||
target: Room | User | RoomMember | MatrixEvent;
|
||||
permalinkCreator: RoomPermalinkCreator;
|
||||
}
|
||||
|
||||
@@ -121,8 +120,6 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
} else if (this.props.target instanceof Group) {
|
||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
|
||||
@@ -153,8 +150,6 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
} else if (this.props.target instanceof Group) {
|
||||
title = _t('Share Community');
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
title = _t('Share Room Message');
|
||||
checkbox = <div>
|
||||
|
||||
@@ -28,7 +28,6 @@ import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserS
|
||||
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
|
||||
import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
|
||||
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
|
||||
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
@@ -41,7 +40,6 @@ import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsT
|
||||
export enum UserTab {
|
||||
General = "USER_GENERAL_TAB",
|
||||
Appearance = "USER_APPEARANCE_TAB",
|
||||
Flair = "USER_FLAIR_TAB",
|
||||
Notifications = "USER_NOTIFICATIONS_TAB",
|
||||
Preferences = "USER_PREFERENCES_TAB",
|
||||
Keyboard = "USER_KEYBOARD_TAB",
|
||||
@@ -103,15 +101,6 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
|
||||
<AppearanceUserSettingsTab />,
|
||||
"UserSettingsAppearance",
|
||||
));
|
||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||
tabs.push(new Tab(
|
||||
UserTab.Flair,
|
||||
_td("Flair"),
|
||||
"mx_UserSettingsDialog_flairIcon",
|
||||
<FlairUserSettingsTab />,
|
||||
"UserSettingFlair",
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
UserTab.Notifications,
|
||||
_td("Notifications"),
|
||||
|
||||
Reference in New Issue
Block a user