Delete groups (legacy communities system) (#8027)

* Remove deprecated feature_communities_v2_prototypes

* Update _components

* i18n

* delint

* Cut out a bit more dead code

* Carve into legacy components

* Carve into mostly the room list code

* Carve into instances of "groupId"

* Carve out more of what comes up with "groups"

* Carve out some settings

* ignore related groups state

* Remove instances of spacesEnabled

* Fix some obvious issues

* Remove now-unused css

* Fix variable naming for legacy components

* Update i18n

* Misc cleanup from manual review

* Update snapshot for changed flag

* Appease linters

* rethemedex

* Remove now-unused AddressPickerDialog

* Make ConfirmUserActionDialog's member a required prop

* Remove useless override from RightPanelStore

* Remove extraneous CSS

* Update i18n

* Demo: "Communities are now Spaces" landing page

* Restore linkify for group IDs

* Demo: Dialog on click for communities->spaces notice

* i18n for demos

* i18n post-merge

* Update copy

* Appease the linter

* Post-merge cleanup

* Re-add spaces_learn_more_url to the new SdkConfig place

* Round 1 of post-merge fixes

* i18n

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Travis Ralston 2022-03-22 17:07:37 -06:00 committed by GitHub
parent 03c80707c9
commit fce36ec826
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
171 changed files with 317 additions and 12160 deletions

View file

@ -1,59 +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.
*/
export const CreateEventField = "io.element.migrated_from_community";
export interface IGroupRoom {
displayname: string;
name?: string;
roomId: string;
canonicalAlias?: string;
avatarUrl?: string;
topic?: string;
numJoinedMembers?: number;
worldReadable?: boolean;
guestCanJoin?: boolean;
isPublic?: boolean;
}
/* eslint-disable camelcase */
export interface IGroupSummary {
profile: {
avatar_url?: string;
is_openly_joinable?: boolean;
is_public?: boolean;
long_description: string;
name: string;
short_description: string;
};
rooms_section: {
rooms: unknown[];
categories: Record<string, unknown>;
total_room_count_estimate: number;
};
user: {
is_privileged: boolean;
is_public: boolean;
is_publicised: boolean;
membership: string;
};
users_section: {
users: unknown[];
roles: Record<string, unknown>;
total_user_count_estimate: number;
};
}
/* eslint-enable camelcase */

View file

@ -27,6 +27,7 @@ import * as sdk from './index';
import { SnakedObject } from "./utils/SnakedObject";
import { IConfigOptions } from "./IConfigOptions";
// Note: we keep the analytics redaction on groups in case people have old links.
const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password|home|directory)/;
const hashVarRegex = /#\/(group|room|user)\/.*$/;
@ -447,7 +448,7 @@ export class Analytics {
</table>
<div>
{ _t('Where this page includes identifiable information, such as a room, '
+ 'user or group ID, that data is removed before being sent to the server.') }
+ 'user ID, that data is removed before being sent to the server.') }
</div>
</div>,
});

View file

@ -22,7 +22,6 @@ import { split } from "lodash";
import DMRoomMap from './utils/DMRoomMap';
import { mediaFromMxc } from "./customisations/Media";
import SpaceStore from "./stores/spaces/SpaceStore";
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(
@ -140,7 +139,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
}
// space rooms cannot be DMs so skip the rest
if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null;
if (room.isSpaceRoom()) return null;
// If the room is not a DM don't fallback to a member avatar
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) return null;

View file

@ -1,162 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import Modal from './Modal';
import * as sdk from './';
import MultiInviter from './utils/MultiInviter';
import { _t } from './languageHandler';
import { MatrixClientPeg } from './MatrixClientPeg';
import GroupStore from './stores/GroupStore';
import StyledCheckbox from './components/views/elements/StyledCheckbox';
export function showGroupInviteDialog(groupId) {
return new Promise((resolve, reject) => {
const description = <div>
<div>{ _t("Who would you like to add to this community?") }</div>
<div className="warning">
{ _t(
"Warning: any person you add to a community will be publicly "+
"visible to anyone who knows the community ID",
) }
</div>
</div>;
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
title: _t("Invite new community members"),
description: description,
placeholder: _t("Name or Matrix ID"),
button: _t("Invite to Community"),
validAddressTypes: ['mx-user-id'],
onFinished: (success, addrs) => {
if (!success) return;
_onGroupInviteFinished(groupId, addrs).then(resolve, reject);
},
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
});
}
export function showGroupAddRoomDialog(groupId) {
return new Promise((resolve, reject) => {
let addRoomsPublicly = false;
const onCheckboxClicked = (e) => {
addRoomsPublicly = e.target.checked;
};
const description = <div>
<div>{ _t("Which rooms would you like to add to this community?") }</div>
</div>;
const checkboxContainer = <StyledCheckbox
className="mx_GroupAddressPicker_checkboxContainer"
onChange={onCheckboxClicked}
>
{ _t("Show these rooms to non-members on the community page and room list?") }
</StyledCheckbox>;
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
title: _t("Add rooms to the community"),
description: description,
extraNode: checkboxContainer,
placeholder: _t("Room name or address"),
button: _t("Add to community"),
pickerType: 'room',
validAddressTypes: ['mx-room-id'],
onFinished: (success, addrs) => {
if (!success) return;
_onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject);
},
}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
});
}
function _onGroupInviteFinished(groupId, addrs) {
const multiInviter = new MultiInviter(groupId);
const addrTexts = addrs.map((addr) => addr.address);
return multiInviter.invite(addrTexts).then((completionStates) => {
// Show user any errors
const errorList = [];
for (const addr of Object.keys(completionStates)) {
if (addrs[addr] === "error") {
errorList.push(addr);
}
}
if (errorList.length > 0) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, {
title: _t("Failed to invite the following users to %(groupId)s:", { groupId: groupId }),
description: errorList.join(", "),
});
}
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, {
title: _t("Failed to invite users to community"),
description: _t("Failed to invite users to %(groupId)s", { groupId: groupId }),
});
});
}
function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
const matrixClient = MatrixClientPeg.get();
const errorList = [];
return Promise.allSettled(addrs.map((addr) => {
return GroupStore
.addRoomToGroup(groupId, addr.address, addRoomsPublicly)
.catch(() => { errorList.push(addr.address); })
.then(() => {
const roomId = addr.address;
const room = matrixClient.getRoom(roomId);
// Can the user change related groups?
if (!room || !room.currentState.mayClientSendStateEvent("m.room.related_groups", matrixClient)) {
return;
}
// Get the related groups
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
const groups = relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [];
// Add this group as related
if (!groups.includes(groupId)) {
groups.push(groupId);
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', { groups }, '');
}
});
})).then(() => {
if (errorList.length === 0) {
return;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog(
'Failed to add the following room to the group',
'',
ErrorDialog,
{
title: _t(
"Failed to add the following rooms to %(groupId)s:",
{ groupId },
),
description: errorList.join(", "),
},
);
});
}

View file

@ -178,6 +178,9 @@ export interface IConfigOptions {
sync_timeline_limit?: number;
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
// XXX: Undocumented URL for the "Learn more about spaces" link in the "Communities don't exist" messaging.
spaces_learn_more_url?: string;
}
export interface ISsoRedirectOptions {

View file

@ -20,8 +20,7 @@ enum PageType {
HomePage = "home_page",
RoomView = "room_view",
UserView = "user_view",
GroupView = "group_view",
MyGroups = "my_groups",
LegacyGroupView = "legacy_group_view",
}
export default PageType;

View file

@ -58,7 +58,7 @@ export enum Anonymity {
const whitelistedScreens = new Set([
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
"start_sso", "start_cas", "complete_security", "post_registration", "room", "user",
]);
export function getRedactedCurrentLocation(

View file

@ -40,8 +40,7 @@ const loggedInPageTypeMap: Record<PageType, ScreenName> = {
[PageType.HomePage]: "Home",
[PageType.RoomView]: "Room",
[PageType.UserView]: "User",
[PageType.GroupView]: "Group",
[PageType.MyGroups]: "MyGroups",
[PageType.LegacyGroupView]: "Group",
};
export default class PosthogTrackers {

View file

@ -25,8 +25,6 @@ import MultiInviter, { CompletionStates } from './utils/MultiInviter';
import Modal from './Modal';
import { _t } from './languageHandler';
import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog";
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore";
import BaseAvatar from "./components/views/avatars/BaseAvatar";
import { mediaFromMxc } from "./customisations/Media";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
@ -78,23 +76,6 @@ export function showRoomInviteDialog(roomId: string, initialText = ""): void {
);
}
export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void {
Modal.createTrackedDialog(
'Invite Users to Community', '', CommunityPrototypeInviteDialog, { communityName, roomId },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
);
}
export function showCommunityInviteDialog(communityId: string): void {
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
if (chat) {
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
showCommunityRoomInviteDialog(chat.roomId, name);
} else {
throw new Error("Failed to locate appropriate room to start an invite in");
}
}
/**
* Checks if the given MatrixEvent is a valid 3rd party user invite.
* @param {MatrixEvent} event The event to check

View file

@ -40,6 +40,7 @@ export const DEFAULTS: Partial<IConfigOptions> = {
logo: require("../res/img/element-desktop-logo.svg").default,
url: "https://element.io/get-started",
},
spaces_learn_more_url: "https://element.io/blog/spaces-blast-out-of-beta/",
};
export default class SdkConfig {

View file

@ -639,7 +639,7 @@ export const Commands = [
return reject(this.getUsage());
}
// If for some reason someone wanted to join a group or user, we should
// If for some reason someone wanted to join a user, we should
// stop them now.
if (!permalinkParts.roomIdOrAlias) {
return reject(this.getUsage());

View file

@ -274,36 +274,6 @@ function textForGuestAccessEvent(ev: MatrixEvent): () => string | null {
}
}
function textForRelatedGroupsEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const groups = ev.getContent().groups || [];
const prevGroups = ev.getPrevContent().groups || [];
const added = groups.filter((g) => !prevGroups.includes(g));
const removed = prevGroups.filter((g) => !groups.includes(g));
if (added.length && !removed.length) {
return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
senderDisplayName,
groups: added.join(', '),
});
} else if (!added.length && removed.length) {
return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
senderDisplayName,
groups: removed.join(', '),
});
} else if (added.length && removed.length) {
return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
'%(oldGroups)s in this room.', {
senderDisplayName,
newGroups: added.join(', '),
oldGroups: removed.join(', '),
});
} else {
// Don't bother rendering this change (because there were no changes)
return null;
}
}
function textForServerACLEvent(ev: MatrixEvent): () => string | null {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const prevContent = ev.getPrevContent();
@ -800,7 +770,6 @@ const stateHandlers: IHandlers = {
[EventType.RoomTombstone]: textForTombstoneEvent,
[EventType.RoomJoinRules]: textForJoinRulesEvent,
[EventType.RoomGuestAccess]: textForGuestAccessEvent,
'm.room.related_groups': textForRelatedGroupsEvent,
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
'im.vector.modular.widgets': textForWidgetEvent,

View file

@ -1,35 +0,0 @@
/*
Copyright 2017 New Vector Ltd
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 { MatrixClient } from "matrix-js-sdk/src/client";
import { asyncAction } from './actionCreators';
import { AsyncActionPayload } from "../dispatcher/payloads";
export default class GroupActions {
/**
* Creates an action thunk that will do an asynchronous request to fetch
* the groups to which a user is joined.
*
* @param {MatrixClient} matrixClient the matrix client to query.
* @returns {AsyncActionPayload} An async action payload.
* @see asyncAction
*/
public static fetchJoinedGroups(matrixClient: MatrixClient): AsyncActionPayload {
return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups(), null);
}
}

View file

@ -1,111 +0,0 @@
/*
Copyright 2017 New Vector Ltd
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 { MatrixClient } from "matrix-js-sdk/src/client";
import Analytics from '../Analytics';
import { asyncAction } from './actionCreators';
import GroupFilterOrderStore from '../stores/GroupFilterOrderStore';
import { AsyncActionPayload } from "../dispatcher/payloads";
export default class TagOrderActions {
/**
* Creates an action thunk that will do an asynchronous request to
* move a tag in GroupFilterOrderStore to destinationIx.
*
* @param {MatrixClient} matrixClient the matrix client to set the
* account data on.
* @param {string} tag the tag to move.
* @param {number} destinationIx the new position of the tag.
* @returns {AsyncActionPayload} an async action payload that will
* dispatch actions indicating the status of the request.
* @see asyncAction
*/
public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload {
// Only commit tags if the state is ready, i.e. not null
let tags = GroupFilterOrderStore.getOrderedTags();
let removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || [];
if (!tags) {
return;
}
tags = tags.filter((t) => t !== tag);
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
removedTags = removedTags.filter((t) => t !== tag);
const storeId = GroupFilterOrderStore.getStoreId();
return asyncAction('TagOrderActions.moveTag', () => {
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
return matrixClient.setAccountData(
'im.vector.web.tag_ordering',
{ tags, removedTags, _storeId: storeId },
);
}, () => {
// For an optimistic update
return { tags, removedTags };
});
}
/**
* Creates an action thunk that will do an asynchronous request to
* label a tag as removed in im.vector.web.tag_ordering account data.
*
* The reason this is implemented with new state `removedTags` is that
* we incrementally and initially populate `tags` with groups that
* have been joined. If we remove a group from `tags`, it will just
* get added (as it looks like a group we've recently joined).
*
* NB: If we ever support adding of tags (which is planned), we should
* take special care to remove the tag from `removedTags` when we add
* it.
*
* @param {MatrixClient} matrixClient the matrix client to set the
* account data on.
* @param {string} tag the tag to remove.
* @returns {function} an async action payload that will dispatch
* actions indicating the status of the request.
* @see asyncAction
*/
public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload {
// Don't change tags, just removedTags
const tags = GroupFilterOrderStore.getOrderedTags();
const removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || [];
if (removedTags.includes(tag)) {
// Return a thunk that doesn't do anything, we don't even need
// an asynchronous action here, the tag is already removed.
return new AsyncActionPayload(() => {});
}
removedTags.push(tag);
const storeId = GroupFilterOrderStore.getStoreId();
return asyncAction('TagOrderActions.removeTag', () => {
Analytics.trackEvent('TagOrderActions', 'removeTag');
return matrixClient.setAccountData(
'im.vector.web.tag_ordering',
{ tags, removedTags, _storeId: storeId },
);
}, () => {
// For an optimistic update
return { removedTags };
});
}
}

View file

@ -19,7 +19,6 @@ import { ReactElement } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import CommandProvider from './CommandProvider';
import CommunityProvider from './CommunityProvider';
import RoomProvider from './RoomProvider';
import UserProvider from './UserProvider';
import EmojiProvider from './EmojiProvider';
@ -27,7 +26,6 @@ import NotifProvider from './NotifProvider';
import { timeout } from "../utils/promise";
import AutocompleteProvider, { ICommand } from "./AutocompleteProvider";
import SpaceProvider from "./SpaceProvider";
import SpaceStore from "../stores/spaces/SpaceStore";
import { TimelineRenderingType } from '../contexts/RoomContext';
export interface ISelectionRange {
@ -55,14 +53,9 @@ const PROVIDERS = [
EmojiProvider,
NotifProvider,
CommandProvider,
SpaceProvider,
];
if (SpaceStore.spacesEnabled) {
PROVIDERS.push(SpaceProvider);
} else {
PROVIDERS.push(CommunityProvider);
}
// Providers will get rejected if they take longer than this.
const PROVIDER_COMPLETION_TIMEOUT = 3000;

View file

@ -1,129 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import Group from "matrix-js-sdk/src/models/group";
import { sortBy } from "lodash";
import { Room } from 'matrix-js-sdk/src/models/room';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import { MatrixClientPeg } from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher';
import { PillCompletion } from './Components';
import { makeGroupPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import FlairStore from "../stores/FlairStore";
import { mediaFromMxc } from "../customisations/Media";
import BaseAvatar from '../components/views/avatars/BaseAvatar';
import { TimelineRenderingType } from '../contexts/RoomContext';
const COMMUNITY_REGEX = /\B\+\S*/g;
function score(query, space) {
const index = space.indexOf(query);
if (index === -1) {
return Infinity;
} else {
return index;
}
}
export default class CommunityProvider extends AutocompleteProvider {
matcher: QueryMatcher<Group>;
constructor(room: Room, renderingType?: TimelineRenderingType) {
super({ commandRegex: COMMUNITY_REGEX, renderingType });
this.matcher = new QueryMatcher([], {
keys: ['groupId', 'name', 'shortDescription'],
});
}
async getCompletions(
query: string,
selection: ISelectionRange,
force = false,
limit = -1,
): Promise<ICompletion[]> {
// Disable autocompletions when composing commands because of various issues
// (see https://github.com/vector-im/element-web/issues/4762)
if (/^(\/join|\/leave)/.test(query)) {
return [];
}
const cli = MatrixClientPeg.get();
let completions = [];
const { command, range } = this.getCurrentCommand(query, selection, force);
if (command) {
const joinedGroups = cli.getGroups().filter(({ myMembership }) => myMembership === 'join');
const groups = (await Promise.all(joinedGroups.map(async ({ groupId }) => {
try {
return FlairStore.getGroupProfileCached(cli, groupId);
} catch (e) { // if FlairStore failed, fall back to just groupId
return Promise.resolve({
name: '',
groupId,
avatarUrl: '',
shortDescription: '',
});
}
})));
this.matcher.setObjects(groups);
const matchedString = command[0];
completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [
(c) => score(matchedString, c.groupId),
(c) => c.groupId.length,
]).map(({ avatarUrl, groupId, name }) => ({
completion: groupId,
suffix: ' ',
type: "community",
href: makeGroupPermalink(groupId),
component: (
<PillCompletion title={name} description={groupId}>
<BaseAvatar
name={name || groupId}
width={24}
height={24}
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null} />
</PillCompletion>
),
range,
})).slice(0, 4);
}
return completions;
}
getName() {
return '💬 ' + _t('Communities');
}
renderCompletions(completions: React.ReactNode[]): React.ReactNode {
return (
<div
className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate"
role="presentation"
aria-label={_t("Community Autocomplete")}
>
{ completions }
</div>
);
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import React from "react";
import { uniqBy, sortBy } from "lodash";
import { sortBy, uniqBy } from "lodash";
import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler';
@ -28,7 +28,6 @@ import { PillCompletion } from './Components';
import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar';
import SpaceStore from "../stores/spaces/SpaceStore";
import { TimelineRenderingType } from "../contexts/RoomContext";
const ROOM_REGEX = /\B#\S*/g;
@ -58,14 +57,9 @@ export default class RoomProvider extends AutocompleteProvider {
protected getRooms() {
const cli = MatrixClientPeg.get();
let rooms = cli.getVisibleRooms();
// if spaces are enabled then filter them out here as they get their own autocomplete provider
if (SpaceStore.spacesEnabled) {
rooms = rooms.filter(r => !r.isSpaceRoom());
}
return rooms;
// filter out spaces here as they get their own autocomplete provider
return cli.getVisibleRooms().filter(r => !r.isSpaceRoom());
}
async getCompletions(

View file

@ -1,107 +0,0 @@
/*
Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import CustomRoomTagStore from '../../stores/CustomRoomTagStore';
import AutoHideScrollbar from './AutoHideScrollbar';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import * as FormattingUtils from '../../utils/FormattingUtils';
import { replaceableComponent } from "../../utils/replaceableComponent";
@replaceableComponent("structures.CustomRoomTagPanel")
class CustomRoomTagPanel extends React.Component {
constructor(props) {
super(props);
this.state = {
tags: CustomRoomTagStore.getSortedTags(),
};
}
componentDidMount() {
this._tagStoreToken = CustomRoomTagStore.addListener(() => {
this.setState({ tags: CustomRoomTagStore.getSortedTags() });
});
}
componentWillUnmount() {
if (this._tagStoreToken) {
this._tagStoreToken.remove();
}
}
render() {
const tags = this.state.tags.map((tag) => {
return (<CustomRoomTagTile tag={tag} key={tag.name} />);
});
const classes = classNames('mx_CustomRoomTagPanel', {
mx_CustomRoomTagPanel_empty: this.state.tags.length === 0,
});
return (<div className={classes}>
<div className="mx_CustomRoomTagPanel_divider" />
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
{ tags }
</AutoHideScrollbar>
</div>);
}
}
class CustomRoomTagTile extends React.Component {
onClick = () => {
dis.dispatch({ action: 'select_custom_room_tag', tag: this.props.tag.name });
};
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
const tag = this.props.tag;
const avatarHeight = 40;
const className = classNames({
"CustomRoomTagPanel_tileSelected": tag.selected,
});
const name = tag.name;
const badgeNotifState = tag.badgeNotifState;
let badgeElement;
if (badgeNotifState) {
const badgeClasses = classNames({
"mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
});
badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badgeNotifState.count) }</div>);
}
return (
<AccessibleTooltipButton className={className} onClick={this.onClick} title={name}>
<div className="mx_TagTile_avatar">
<BaseAvatar
name={tag.avatarLetter}
idName={name}
width={avatarHeight}
height={avatarHeight}
/>
{ badgeElement }
</div>
</AccessibleTooltipButton>
);
}
}
export default CustomRoomTagPanel;

View file

@ -1,182 +0,0 @@
/*
Copyright 2017, 2018 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import { ClientEvent } from "matrix-js-sdk/src/client";
import type { EventSubscription } from "fbemitter";
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
import GroupActions from '../../actions/GroupActions';
import dis from '../../dispatcher/dispatcher';
import { _t } from '../../languageHandler';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import SettingsStore from "../../settings/SettingsStore";
import UserTagTile from "../views/elements/UserTagTile";
import { replaceableComponent } from "../../utils/replaceableComponent";
import UIStore from "../../stores/UIStore";
import DNDTagTile from "../views/elements/DNDTagTile";
import ActionButton from "../views/elements/ActionButton";
interface IGroupFilterPanelProps {
}
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
type OrderedTagsTemporaryType = Array<{}>;
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
type SelectedTagsTemporaryType = Array<{}>;
interface IGroupFilterPanelState {
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
orderedTags: OrderedTagsTemporaryType;
// FIXME: Properly type this after migrating GroupFilterOrderStore.js to Typescript
selectedTags: SelectedTagsTemporaryType;
}
@replaceableComponent("structures.GroupFilterPanel")
class GroupFilterPanel extends React.Component<IGroupFilterPanelProps, IGroupFilterPanelState> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
public state = {
orderedTags: [],
selectedTags: [],
};
private ref = React.createRef<HTMLDivElement>();
private unmounted = false;
private groupFilterOrderStoreToken?: EventSubscription;
public componentDidMount() {
this.unmounted = false;
this.context.on(ClientEvent.GroupMyMembership, this.onGroupMyMembership);
this.context.on(ClientEvent.Sync, this.onClientSync);
this.groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => {
if (this.unmounted) {
return;
}
this.setState({
orderedTags: GroupFilterOrderStore.getOrderedTags() || [],
selectedTags: GroupFilterOrderStore.getSelectedTags(),
});
});
// This could be done by anything with a matrix client
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
UIStore.instance.trackElementDimensions("GroupPanel", this.ref.current);
}
public componentWillUnmount() {
this.unmounted = true;
this.context.removeListener(ClientEvent.GroupMyMembership, this.onGroupMyMembership);
this.context.removeListener(ClientEvent.Sync, this.onClientSync);
if (this.groupFilterOrderStoreToken) {
this.groupFilterOrderStoreToken.remove();
}
UIStore.instance.stopTrackingElementDimensions("GroupPanel");
}
private onGroupMyMembership = () => {
if (this.unmounted) return;
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
};
private onClientSync = (syncState, prevState) => {
// Consider the client reconnected if there is no error with syncing.
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
const reconnected = syncState !== "ERROR" && prevState !== syncState;
if (reconnected) {
// Load joined groups
dis.dispatch(GroupActions.fetchJoinedGroups(this.context));
}
};
private onClick = e => {
// only dispatch if its not a no-op
if (this.state.selectedTags.length > 0) {
dis.dispatch({ action: 'deselect_tags' });
}
};
private onClearFilterClick = ev => {
dis.dispatch({ action: 'deselect_tags' });
};
private renderGlobalIcon() {
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
return (
<div>
<UserTagTile />
<hr className="mx_GroupFilterPanel_divider" />
</div>
);
}
public render() {
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
key={tag}
tag={tag}
index={index}
selected={this.state.selectedTags.includes(tag)}
/>;
});
const itemsSelected = this.state.selectedTags.length > 0;
const classes = classNames('mx_GroupFilterPanel', {
mx_GroupFilterPanel_items_selected: itemsSelected,
});
let createButton = (
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus"
/>
);
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
createButton = (
<ActionButton
tooltip
label={_t("Create community")}
action="view_create_group"
className="mx_TagTile mx_TagTile_plus" />
);
}
return <div className={classes} onClick={this.onClearFilterClick} ref={this.ref}>
<AutoHideScrollbar
className="mx_GroupFilterPanel_scroller"
onClick={this.onClick}
>
<div className="mx_GroupFilterPanel_tagTileContainer">
{ this.renderGlobalIcon() }
{ tags }
<div>
{ createButton }
</div>
</div>
</AutoHideScrollbar>
</div>;
}
}
export default GroupFilterPanel;

File diff suppressed because it is too large Load diff

View file

@ -42,7 +42,6 @@ import IndicatorScrollbar from "./IndicatorScrollbar";
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
import SettingsStore from "../../settings/SettingsStore";
import VoiceChannelRadio from "../views/voip/VoiceChannelRadio";
import UserMenu from "./UserMenu";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
import { UIComponent } from "../../settings/UIFeature";
@ -385,7 +384,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
>
{ !SpaceStore.spacesEnabled && <UserMenu isPanelCollapsed={true} /> }
<RoomSearch
isMinimized={this.props.isMinimized}
ref={this.roomSearchRef}
@ -429,7 +427,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
{ !this.props.isMinimized && (
<RoomListHeader
onVisibilityChange={this.refreshStickyHeaders}
spacePanelDisabled={!SpaceStore.spacesEnabled}
/>
) }
<div className="mx_LeftPanel_roomListWrapper">

View file

@ -1,115 +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, { useContext } from "react";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { _t } from "../../languageHandler";
import AccessibleButton from "../views/elements/AccessibleButton";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
import Spinner from "../views/elements/Spinner";
import GroupAvatar from "../views/avatars/GroupAvatar";
import { linkifyElement } from "../../HtmlUtils";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { IGroupSummary } from "../../@types/groups";
interface IProps {
groupId: string;
}
const onSwapClick = () => {
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Preferences,
});
};
// XXX: temporary community migration component, reuses SpaceRoomView & SpacePreview classes for simplicity
const LegacyCommunityPreview = ({ groupId }: IProps) => {
const cli = useContext(MatrixClientContext);
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [cli, groupId]);
if (!groupSummary) {
return <main className="mx_SpaceRoomView">
<div className="mx_MainSplit">
<div className="mx_SpaceRoomView_preview">
<Spinner />
</div>
</div>
</main>;
}
let visibilitySection: JSX.Element;
if (groupSummary.profile.is_public) {
visibilitySection = <span className="mx_SpaceRoomView_info_public">
{ _t("Public community") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_info_private">
{ _t("Private community") }
</span>;
}
return <main className="mx_SpaceRoomView">
<ErrorBoundary>
<div className="mx_MainSplit">
<div className="mx_SpaceRoomView_preview">
<GroupAvatar
groupId={groupId}
groupName={groupSummary.profile.name}
groupAvatarUrl={groupSummary.profile.avatar_url}
height={80}
width={80}
resizeMethod='crop'
/>
<h1 className="mx_SpaceRoomView_preview_name">
{ groupSummary.profile.name }
</h1>
<div className="mx_SpaceRoomView_info">
{ visibilitySection }
</div>
<div className="mx_SpaceRoomView_preview_topic" ref={e => e && linkifyElement(e)}>
{ groupSummary.profile.short_description }
</div>
<div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
{ groupSummary.user?.membership === "join"
? _t("To view %(communityName)s, swap to communities in your <a>preferences</a>", {
communityName: groupSummary.profile.name,
}, {
a: sub => (
<AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
),
})
: _t("To join %(communityName)s, swap to communities in your <a>preferences</a>", {
communityName: groupSummary.profile.name,
}, {
a: sub => (
<AccessibleButton onClick={onSwapClick} kind="link">{ sub }</AccessibleButton>
),
})
}
</div>
</div>
</div>
</ErrorBoundary>
</main>;
};
export default LegacyCommunityPreview;

View file

@ -0,0 +1,51 @@
/*
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 * as React from "react";
import AutoHideScrollbar from './AutoHideScrollbar';
import { _t } from "../../languageHandler";
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
interface IProps {
groupId: string;
}
const LegacyGroupView: React.FC<IProps> = ({ groupId }) => {
// XXX: Stealing classes from the HomePage component for CSS simplicity.
// XXX: Inline CSS because this is all temporary
const learnMoreUrl = SdkConfig.get().spaces_learn_more_url ?? DEFAULTS.spaces_learn_more_url;
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
<div className="mx_HomePage_default_wrapper">
<h1 style={{ fontSize: '24px' }}>{ _t("That link is no longer supported") }</h1>
<p>
{ _t(
"You're trying to access a community link (%(groupId)s).<br/>" +
"Communities are no longer supported and have been replaced by spaces.<br2/>" +
"<a>Learn more about spaces here.</a>",
{ groupId },
{
br: () => <br />,
br2: () => <br />,
a: (sub) => <a href={learnMoreUrl} rel="noreferrer noopener" target="_blank">{ sub }</a>,
},
) }
</p>
</div>
</AutoHideScrollbar>;
};
export default LegacyGroupView;

View file

@ -60,21 +60,16 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
import RoomView from './RoomView';
import type { RoomView as RoomViewType } from './RoomView';
import ToastContainer from './ToastContainer';
import MyGroups from "./MyGroups";
import UserView from "./UserView";
import GroupView from "./GroupView";
import BackdropPanel from "./BackdropPanel";
import SpaceStore from "../../stores/spaces/SpaceStore";
import GroupFilterPanel from './GroupFilterPanel';
import CustomRoomTagPanel from './CustomRoomTagPanel';
import { mediaFromMxc } from "../../customisations/Media";
import LegacyCommunityPreview from "./LegacyCommunityPreview";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RightPanelStore from '../../stores/right-panel/RightPanelStore';
import { TimelineRenderingType } from "../../contexts/RoomContext";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload";
import LegacyGroupView from "./LegacyGroupView";
import { IConfigOptions } from "../../IConfigOptions";
import LeftPanelLiveShareWarning from '../views/beacon/LeftPanelLiveShareWarning';
@ -106,11 +101,11 @@ interface IProps {
collapseLhs: boolean;
config: IConfigOptions;
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
justRegistered?: boolean;
roomJustCreatedOpts?: IOpts;
forceTimeline?: boolean; // see props on MatrixChat
currentGroupId?: string;
}
interface IState {
@ -499,7 +494,7 @@ class LoggedInView extends React.Component<IProps, IState> {
handled = true;
break;
case KeyBindingAction.ToggleRoomSidePanel:
if (this.props.page_type === "room_view" || this.props.page_type === "group_view") {
if (this.props.page_type === "room_view") {
RightPanelStore.instance.togglePanel();
handled = true;
}
@ -570,7 +565,6 @@ class LoggedInView extends React.Component<IProps, IState> {
if (
!handled &&
PlatformPeg.get().overrideBrowserShortcuts() &&
SpaceStore.spacesEnabled &&
ev.code.startsWith("Digit") &&
ev.code !== "Digit0" && // this is the shortcut for reset zoom, don't override it
isOnlyCtrlOrCmdKeyEvent(ev)
@ -642,10 +636,6 @@ class LoggedInView extends React.Component<IProps, IState> {
/>;
break;
case PageTypes.MyGroups:
pageElement = <MyGroups />;
break;
case PageTypes.HomePage:
pageElement = <HomePage justRegistered={this.props.justRegistered} />;
break;
@ -653,16 +643,9 @@ class LoggedInView extends React.Component<IProps, IState> {
case PageTypes.UserView:
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
break;
case PageTypes.GroupView:
if (SpaceStore.spacesEnabled) {
pageElement = <LegacyCommunityPreview groupId={this.props.currentGroupId} />;
} else {
pageElement = <GroupView
groupId={this.props.currentGroupId}
isNew={this.props.currentGroupIsNew}
resizeNotifier={this.props.resizeNotifier}
/>;
}
case PageTypes.LegacyGroupView:
pageElement = <LegacyGroupView groupId={this.props.currentGroupId} />;
break;
}
@ -694,23 +677,11 @@ class LoggedInView extends React.Component<IProps, IState> {
<div className='mx_LeftPanel_outerWrapper'>
<LeftPanelLiveShareWarning isMinimized={this.props.collapseLhs || false} />
<div className='mx_LeftPanel_wrapper'>
{ SettingsStore.getValue('TagPanel.enableTagPanel') &&
(<div className="mx_GroupFilterPanelContainer">
<BackdropPanel
blurMultiplier={0.5}
backgroundImage={this.state.backgroundImage}
/>
<GroupFilterPanel />
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
</div>)
}
{ SpaceStore.spacesEnabled ? <>
<BackdropPanel
blurMultiplier={0.5}
backgroundImage={this.state.backgroundImage}
/>
<SpacePanel />
</> : null }
<BackdropPanel
blurMultiplier={0.5}
backgroundImage={this.state.backgroundImage}
/>
<SpacePanel />
<BackdropPanel
backgroundImage={this.state.backgroundImage}
/>

View file

@ -84,14 +84,11 @@ import {
} from "../../stores/notifications/RoomNotificationStateStore";
import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership";
import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import { UIFeature } from "../../settings/UIFeature";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { RoomUpdateCause } from "../../stores/room-list/models";
@ -99,7 +96,6 @@ import SecurityCustomisations from "../../customisations/Security";
import Spinner from "../views/elements/Spinner";
import QuestionDialog from "../views/dialogs/QuestionDialog";
import UserSettingsDialog, { UserTab } from '../views/dialogs/UserSettingsDialog';
import CreateGroupDialog from '../views/dialogs/CreateGroupDialog';
import CreateRoomDialog from '../views/dialogs/CreateRoomDialog';
import RoomDirectory from './RoomDirectory';
import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog";
@ -147,7 +143,6 @@ const ONBOARDING_FLOW_STARTERS = [
Action.ViewUserSettings,
'view_create_chat',
'view_create_room',
'view_create_group',
];
interface IScreen {
@ -184,10 +179,10 @@ interface IState {
// in the case where we view a room by ID or by RoomView when it resolves
// what ID an alias points at.
currentRoomId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
// If we're trying to just view a user ID (i.e. /user URL), this is it
currentUserId?: string;
// Group ID for legacy "communities don't exist" page
currentGroupId?: string;
// this is persisted as mx_lhs_size, loaded in LoggedInView
collapseLhs: boolean;
// Parameters used in the registration dance with the IS
@ -668,6 +663,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
break;
}
case 'view_legacy_group':
this.viewLegacyGroup(payload.groupId);
break;
case Action.ViewUserSettings: {
const tabPayload = payload as OpenToTabPayload;
Modal.createTrackedDialog('User settings', '', UserSettingsDialog,
@ -684,15 +682,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// View the welcome or home page if we need something to look at
this.viewSomethingBehindModal();
break;
case 'view_create_group': {
const prototype = SettingsStore.getValue("feature_communities_v2_prototypes");
Modal.createTrackedDialog(
'Create Community',
'',
prototype ? CreateCommunityPrototypeDialog : CreateGroupDialog,
);
break;
}
case Action.ViewRoomDirectory: {
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
initialText: payload.initialText,
@ -702,13 +691,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewSomethingBehindModal();
break;
}
case 'view_my_groups':
this.setPage(PageType.MyGroups);
this.notifyNewScreen('groups');
break;
case 'view_group':
this.viewGroup(payload);
break;
case 'view_welcome_page':
this.viewWelcome();
break;
@ -740,17 +722,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// function will have cleared that state and not execute that path.
this.showScreenAfterLogin();
break;
case 'toggle_my_groups':
// persist that the user has interacted with this, use it to dismiss the beta dot
localStorage.setItem("mx_seenSpacesBeta", "1");
// We just dispatch the page change rather than have to worry about
// what the logic is for each of these branches.
if (this.state.page_type === PageType.MyGroups) {
dis.dispatch({ action: 'view_last_screen' });
} else {
dis.dispatch({ action: 'view_my_groups' });
}
break;
case 'hide_left_panel':
this.setState({
collapseLhs: true,
@ -952,33 +923,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
private async viewGroup(payload) {
const groupId = payload.group_id;
// Wait for the first sync to complete
if (!this.firstSyncComplete) {
if (!this.firstSyncPromise) {
logger.warn('Cannot view a group before first sync. group_id:', groupId);
return;
}
await this.firstSyncPromise.promise;
}
this.setState({
view: Views.LOGGED_IN,
currentGroupId: groupId,
currentGroupIsNew: payload.group_is_new,
});
this.setPage(PageType.GroupView);
this.notifyNewScreen('group/' + groupId);
}
private viewSomethingBehindModal() {
if (this.state.view !== Views.LOGGED_IN) {
this.viewWelcome();
return;
}
if (!this.state.currentGroupId && !this.state.currentRoomId && !this.state.currentUserId) {
if (!this.state.currentRoomId && !this.state.currentUserId) {
this.viewHome();
}
}
@ -1034,19 +984,17 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
private async createRoom(defaultPublic = false, defaultName?: string) {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
if (communityId) {
// double check the user will have permission to associate this room with the community
if (!CommunityPrototypeStore.instance.isAdminOf(communityId)) {
Modal.createTrackedDialog('Pre-failure to create room', '', ErrorDialog, {
title: _t("Cannot create rooms in this community"),
description: _t("You do not have permission to create rooms in this community."),
});
return;
}
}
private viewLegacyGroup(groupId: string) {
this.setStateForNewView({
view: Views.LOGGED_IN,
currentRoomId: null,
currentGroupId: groupId,
});
this.notifyNewScreen('group/' + groupId);
this.setPage(PageType.LegacyGroupView);
}
private async createRoom(defaultPublic = false, defaultName?: string) {
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
defaultPublic,
defaultName,
@ -1110,7 +1058,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private leaveRoomWarnings(roomId: string) {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const isSpace = SpaceStore.spacesEnabled && roomToLeave?.isSpaceRoom();
const isSpace = roomToLeave?.isSpaceRoom();
// Show a warning if there are additional complications.
const warnings = [];
@ -1148,7 +1096,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId);
const isSpace = SpaceStore.spacesEnabled && roomToLeave?.isSpaceRoom();
const isSpace = roomToLeave?.isSpaceRoom();
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
title: isSpace ? _t("Leave space") : _t("Leave room"),
description: (
@ -1775,14 +1723,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const type = screen === "start_sso" ? "sso" : "cas";
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
} else if (screen === 'groups') {
if (SpaceStore.spacesEnabled) {
dis.dispatch({ action: Action.ViewHomePage });
return;
}
dis.dispatch({
action: 'view_my_groups',
});
} else if (screen.indexOf('room/') === 0) {
// Rooms can have the following formats:
// #room_alias:domain or !opaque_id:domain
@ -1865,12 +1805,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
} else if (screen.indexOf('group/') === 0) {
const groupId = screen.substring(6);
// TODO: Check valid group ID
dis.dispatch({
action: 'view_group',
group_id: groupId,
action: 'view_legacy_group',
groupId: groupId,
});
} else {
logger.info("Ignoring showScreen for '%s'", screen);

View file

@ -157,9 +157,6 @@ interface IProps {
// which layout to use
layout?: Layout;
// whether or not to show flair at all
enableFlair?: boolean;
resizeNotifier: ResizeNotifier;
permalinkCreator?: RoomPermalinkCreator;
editState?: EditorStateTransfer;
@ -811,7 +808,6 @@ export default class MessagePanel extends React.Component<IProps, IState> {
getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
callEventGrouper={callEventGrouper}
hideSender={this.state.hideSender}

View file

@ -1,147 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../index';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import dis from '../../dispatcher/dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import { replaceableComponent } from "../../utils/replaceableComponent";
@replaceableComponent("structures.MyGroups")
export default class MyGroups extends React.Component {
static contextType = MatrixClientContext;
state = {
groups: null,
error: null,
};
componentDidMount() {
this._fetch();
}
_onCreateGroupClick = () => {
dis.dispatch({ action: 'view_create_group' });
};
_fetch() {
this.context.getJoinedGroups().then((result) => {
this.setState({ groups: result.groups, error: null });
}, (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
// Indicate that the guest isn't in any groups (which should be true)
this.setState({ groups: [], error: null });
return;
}
this.setState({ groups: null, error: err });
});
}
render() {
const brand = SdkConfig.get().brand;
const Loader = sdk.getComponent("elements.Spinner");
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const GroupTile = sdk.getComponent("groups.GroupTile");
let content;
let contentHeader;
if (this.state.groups) {
const groupNodes = [];
this.state.groups.forEach((g) => {
groupNodes.push(<GroupTile key={g} groupId={g} />);
});
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
content = groupNodes.length > 0 ?
<AutoHideScrollbar className="mx_MyGroups_scrollable">
<div className="mx_MyGroups_microcopy">
<p>
{ _t(
"Did you know: you can use communities to filter your %(brand)s experience!",
{ brand },
) }
</p>
<p>
{ _t(
"You can click on an avatar in the " +
"filter panel at any time to see only the rooms and people associated " +
"with that community.",
) }
</p>
</div>
<div className="mx_MyGroups_joinedGroups">
{ groupNodes }
</div>
</AutoHideScrollbar> :
<div className="mx_MyGroups_placeholder">
{ _t(
"You're not currently a member of any communities.",
) }
</div>;
} else if (this.state.error) {
content = <div className="mx_MyGroups_error">
{ _t('Error whilst fetching joined communities') }
</div>;
} else {
content = <Loader />;
}
return <div className="mx_MyGroups">
<SimpleRoomHeader title={_t("Communities")} icon={require("../../../res/img/icons-groups.svg").default} />
<div className='mx_MyGroups_header'>
<div className="mx_MyGroups_headerCard">
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onCreateGroupClick} />
<div className="mx_MyGroups_headerCard_content">
<div className="mx_MyGroups_headerCard_header">
{ _t('Create a new community') }
</div>
{ _t(
'Create a community to group together users and rooms! ' +
'Build a custom homepage to mark out your space in the Matrix universe.',
) }
</div>
</div>
{ /*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
<img src={require("../../../res/img/icons-create-room.svg").default} width="50" height="50" />
</AccessibleButton>
<div className="mx_MyGroups_headerCard_content">
<div className="mx_MyGroups_headerCard_header">
{ _t('Join an existing community') }
</div>
{ _t(
'To join an existing community you\'ll have to '+
'know its community identifier; this will look '+
'something like <i>+example:matrix.org</i>.',
{},
{ 'i': (sub) => <i>{ sub }</i> })
}
</div>
</div>*/ }
</div>
<div className="mx_MyGroups_content">
{ contentHeader }
{ content }
</div>
</div>;
}
}

View file

@ -31,9 +31,6 @@ import WidgetCard from "../views/right_panel/WidgetCard";
import { replaceableComponent } from "../../utils/replaceableComponent";
import SettingsStore from "../../settings/SettingsStore";
import MemberList from "../views/rooms/MemberList";
import GroupMemberList from "../views/groups/GroupMemberList";
import GroupRoomList from "../views/groups/GroupRoomList";
import GroupRoomInfo from "../views/groups/GroupRoomInfo";
import UserInfo from "../views/right_panel/UserInfo";
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
import FilePanel from "./FilePanel";
@ -51,7 +48,6 @@ import { Action } from '../../dispatcher/actions';
interface IProps {
room?: Room; // if showing panels for a given room, this is set
groupId?: string; // if showing panels for a given group, this is set
overwriteCard?: IRightPanelCard; // used to display a custom card and ignoring the RightPanelStore (used for UserView)
resizeNotifier: ResizeNotifier;
permalinkCreator?: RoomPermalinkCreator;
@ -96,9 +92,6 @@ export default class RightPanel extends React.Component<IProps, IState> {
if (props.room) {
currentCard = RightPanelStore.instance.currentCardForRoom(props.room.roomId);
}
if (props.groupId) {
currentCard = RightPanelStore.instance.currentGroup;
}
if (currentCard?.phase && !RightPanelStore.instance.isPhaseValid(currentCard.phase, !!props.room)) {
// XXX: We can probably get rid of this workaround once GroupView is dead, it's unmounting happens weirdly
@ -186,16 +179,6 @@ export default class RightPanel extends React.Component<IProps, IState> {
/>;
break;
case RightPanelPhases.GroupMemberList:
if (this.props.groupId) {
card = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
}
break;
case RightPanelPhases.GroupRoomList:
card = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
break;
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.SpaceMemberInfo:
case RightPanelPhases.EncryptionPanel: {
@ -218,24 +201,6 @@ export default class RightPanel extends React.Component<IProps, IState> {
card = <ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} />;
break;
case RightPanelPhases.GroupMemberInfo:
card = <UserInfo
user={cardState.member}
groupId={this.props.groupId}
key={cardState.member.userId}
phase={phase}
onClose={this.onClose}
/>;
break;
case RightPanelPhases.GroupRoomInfo:
card = <GroupRoomInfo
groupRoomId={cardState.groupRoomId}
groupId={this.props.groupId}
key={cardState.groupRoomId}
/>;
break;
case RightPanelPhases.NotificationPanel:
card = <NotificationPanel onClose={this.onClose} />;
break;

View file

@ -31,9 +31,6 @@ import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/Di
import Analytics from '../../Analytics';
import NetworkDropdown, { ALL_ROOMS, Protocols } from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { mediaFromMxc } from "../../customisations/Media";
import { IDialogProps } from "../views/dialogs/IDialogProps";
@ -72,8 +69,6 @@ interface IState {
instanceId: string;
roomServer: string;
filterString: string;
selectedCommunityId?: string;
communityName?: string;
}
@replaceableComponent("structures.RoomDirectory")
@ -86,15 +81,11 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
constructor(props) {
super(props);
const selectedCommunityId = SettingsStore.getValue("feature_communities_v2_prototypes")
? GroupFilterOrderStore.getSelectedTags()[0]
: null;
let protocolsLoading = true;
if (!MatrixClientPeg.get()) {
// We may not have a client yet when invoked from welcome page
protocolsLoading = false;
} else if (!selectedCommunityId) {
} else {
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response;
const myHomeserver = MatrixClientPeg.getHomeserverName();
@ -147,14 +138,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
),
});
});
} else {
// We don't use the protocols in the communities v2 prototype experience
protocolsLoading = false;
// Grab the profile info async
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
this.setState({ communityName: profile.name });
});
}
this.state = {
@ -164,8 +147,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
instanceId: localStorage.getItem(LAST_INSTANCE_KEY),
roomServer: localStorage.getItem(LAST_SERVER_KEY),
filterString: this.props.initialText || "",
selectedCommunityId,
communityName: null,
protocolsLoading,
};
}
@ -182,33 +163,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}
private refreshRoomList = () => {
if (this.state.selectedCommunityId) {
this.setState({
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
return {
// Translate all the group properties to the directory format
room_id: r.roomId,
name: r.name,
topic: r.topic,
canonical_alias: r.canonicalAlias,
num_joined_members: r.numJoinedMembers,
avatarUrl: r.avatarUrl,
world_readable: r.worldReadable,
guest_can_join: r.guestsCanJoin,
};
}).filter(r => {
const filterString = this.state.filterString;
if (filterString) {
const containedIn = (s: string) => (s || "").toLowerCase().includes(filterString.toLowerCase());
return containedIn(r.name) || containedIn(r.topic) || containedIn(r.canonical_alias);
}
return true;
}),
loading: false,
});
return;
}
this.nextBatch = null;
this.setState({
publicRooms: [],
@ -218,7 +172,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
};
private getMoreRooms(): Promise<boolean> {
if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms
if (!MatrixClientPeg.get()) return Promise.resolve(false);
this.setState({
@ -342,7 +295,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: React.MouseEvent) => {
// If room was shift-clicked, remove it from the room directory
if (ev.shiftKey && !this.state.selectedCommunityId) {
if (ev.shiftKey) {
ev.preventDefault();
this.removeFromDirectory(room);
}
@ -755,18 +708,6 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
}
}
let dropdown = (
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
);
if (this.state.selectedCommunityId) {
dropdown = null;
}
listHeader = <div className="mx_RoomDirectory_listheader">
<DirectorySearchBox
className="mx_RoomDirectory_searchbox"
@ -777,7 +718,12 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
showJoinButton={showJoinButton}
initialText={this.props.initialText}
/>
{ dropdown }
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
</div>;
}
const explanation =
@ -789,10 +735,7 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
) },
);
const title = this.state.selectedCommunityId
? _t("Explore rooms in %(communityName)s", {
communityName: this.state.communityName || this.state.selectedCommunityId,
}) : _t("Explore rooms");
const title = _t("Explore rooms");
return (
<BaseDialog
className="mx_RoomDirectory_dialog"

View file

@ -96,7 +96,6 @@ import RoomStatusBar from "./RoomStatusBar";
import MessageComposer from '../views/rooms/MessageComposer';
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { showThread } from '../../dispatcher/dispatch-actions/threads';
import { fetchInitialEvent } from "../../utils/EventUtils";
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
@ -1254,7 +1253,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};
private onInviteButtonClick = () => {
// call AddressPickerDialog
// open the room inviter
dis.dispatch({
action: 'view_invite',
roomId: this.state.room.roomId,
@ -1807,7 +1806,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
const myMembership = this.state.room.getMyMembership();
// SpaceRoomView handles invites itself
if (myMembership === "invite" && (!SpaceStore.spacesEnabled || !this.state.room.isSpaceRoom())) {
if (myMembership === "invite" && !this.state.room.isSpaceRoom()) {
if (this.state.joining || this.state.rejecting) {
return (
<ErrorBoundary>
@ -1938,7 +1937,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
room={this.state.room}
/>
);
if (!this.state.canPeek && (!SpaceStore.spacesEnabled || !this.state.room?.isSpaceRoom())) {
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
return (
<div className="mx_RoomView">
{ previewBar }

View file

@ -57,7 +57,6 @@ import {
} from "../../utils/space";
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
import MemberAvatar from "../views/avatars/MemberAvatar";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { RoomFacePile } from "../views/elements/FacePile";
import {
AddExistingToSpace,
@ -71,12 +70,9 @@ import IconizedContextMenu, {
} from "../views/context_menus/IconizedContextMenu";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { BetaPill } from "../views/beta/BetaCard";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
import Spinner from "../views/elements/Spinner";
import GroupAvatar from "../views/avatars/GroupAvatar";
import { useDispatcher } from "../../hooks/useDispatcher";
import { useRoomState } from "../../hooks/useRoomState";
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
@ -84,7 +80,6 @@ import { UIComponent } from "../../settings/UIFeature";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import PosthogTrackers from "../../PosthogTrackers";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { CreateEventField, IGroupSummary } from "../../@types/groups";
interface IProps {
space: Room;
@ -179,33 +174,6 @@ const SpaceInfo = ({ space }: { space: Room }) => {
</div>;
};
const onPreferencesClick = () => {
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Preferences,
});
};
// XXX: temporary community migration component
const GroupTile = ({ groupId }: { groupId: string }) => {
const cli = useContext(MatrixClientContext);
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [cli, groupId]);
if (!groupSummary) return <Spinner />;
return <>
<GroupAvatar
groupId={groupId}
groupName={groupSummary.profile.name}
groupAvatarUrl={groupSummary.profile.avatar_url}
width={16}
height={16}
resizeMethod='crop'
/>
{ groupSummary.profile.name }
</>;
};
interface ISpacePreviewProps {
space: Room;
onJoinButtonClicked(): void;
@ -223,8 +191,6 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
const [busy, setBusy] = useState(false);
const spacesEnabled = SpaceStore.spacesEnabled;
const joinRule = useRoomState(space, state => state.getJoinRule());
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
&& joinRule !== JoinRule.Public;
@ -282,7 +248,6 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
setBusy(true);
onJoinButtonClicked();
}}
disabled={!spacesEnabled}
>
{ _t("Accept") }
</AccessibleButton>
@ -298,7 +263,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
setBusy(true);
}
}}
disabled={!spacesEnabled || cannotJoin}
disabled={cannotJoin}
>
{ _t("Join") }
</AccessibleButton>
@ -310,18 +275,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
}
let footer;
if (!spacesEnabled) {
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
{ myMembership === "join"
? _t("To view this Space, hide communities in your <a>preferences</a>", {}, {
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
})
: _t("To join this Space, hide communities in your <a>preferences</a>", {}, {
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
})
}
</div>;
} else if (cannotJoin) {
if (cannotJoin) {
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
{ _t("To view %(spaceName)s, you need an invite", {
spaceName: space.name,
@ -329,18 +283,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
</div>;
}
let migratedCommunitySection: JSX.Element;
const createContent = space.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent();
if (createContent[CreateEventField]) {
migratedCommunitySection = <div className="mx_SpaceRoomView_preview_migratedCommunity">
{ _t("Created from <Community />", {}, {
Community: () => <GroupTile groupId={createContent[CreateEventField]} />,
}) }
</div>;
}
return <div className="mx_SpaceRoomView_preview">
{ migratedCommunitySection }
{ inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name">
@ -884,7 +827,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
private renderBody() {
switch (this.state.phase) {
case Phase.Landing:
if (this.state.myMembership === "join" && SpaceStore.spacesEnabled) {
if (this.state.myMembership === "join") {
return <SpaceLanding space={this.props.space} />;
} else {
return <SpacePreview

View file

@ -41,7 +41,6 @@ import { Action } from '../../dispatcher/actions';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import { haveTileForEvent } from "../views/rooms/EventTile";
import { UIFeature } from "../../settings/UIFeature";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
import MessagePanel from "./MessagePanel";
@ -1648,7 +1647,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
editState={this.props.editState}
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
hideThreadedMessages={this.props.hideThreadedMessages}
disableGrouping={this.props.disableGrouping}
callEventGroupers={this.callEventGroupers}

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { createRef, useContext, useRef, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import * as fbEmitter from "fbemitter";
import classNames from "classnames";
import { MatrixClientPeg } from "../../MatrixClientPeg";
@ -50,7 +49,6 @@ import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import { UIFeature } from "../../settings/UIFeature";
import HostSignupAction from "./HostSignupAction";
import SpaceStore from "../../stores/spaces/SpaceStore";
@ -151,7 +149,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
private themeWatcherRef: string;
private readonly dndWatcherRef: string;
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
private tagStoreRef: fbEmitter.EventSubscription;
constructor(props: IProps) {
super(props);
@ -165,9 +162,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
};
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
if (SpaceStore.spacesEnabled) {
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
SettingsStore.monitorSetting("feature_dnd", null);
SettingsStore.monitorSetting("doNotDisturb", null);
@ -184,7 +179,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
public componentDidMount() {
this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
}
public componentWillUnmount() {
@ -192,16 +186,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (this.dndWatcherRef) SettingsStore.unwatchSetting(this.dndWatcherRef);
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
this.tagStoreRef.remove();
if (SpaceStore.spacesEnabled) {
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
}
private onTagStoreUpdate = () => {
this.forceUpdate(); // we don't have anything useful in state to update
};
private isUserOnDarkTheme(): boolean {
if (SettingsStore.getValue("use_system_theme")) {
return window.matchMedia("(prefers-color-scheme: dark)").matches;

View file

@ -1,66 +0,0 @@
/*
Copyright 2017, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import BaseAvatar from './BaseAvatar';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
export interface IProps {
groupId?: string;
groupName?: string;
groupAvatarUrl?: string;
width?: number;
height?: number;
resizeMethod?: ResizeMethod;
onClick?: React.MouseEventHandler;
}
@replaceableComponent("views.avatars.GroupAvatar")
export default class GroupAvatar extends React.Component<IProps> {
public static defaultProps = {
width: 36,
height: 36,
resizeMethod: 'crop',
};
getGroupAvatarUrl() {
if (!this.props.groupAvatarUrl) return null;
return mediaFromMxc(this.props.groupAvatarUrl).getThumbnailOfSourceHttp(
this.props.width,
this.props.height,
this.props.resizeMethod,
);
}
render() {
// extract the props we use from props so we can pass any others through
// should consider adding this as a global rule in js-sdk?
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const { groupId, groupAvatarUrl, groupName, ...otherProps } = this.props;
return (
<BaseAvatar
name={groupName || this.props.groupId[1]}
idName={this.props.groupId}
url={this.getGroupAvatarUrl()}
{...otherProps}
/>
);
}
}

View file

@ -1,93 +0,0 @@
/*
Copyright 2018 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Group } from 'matrix-js-sdk/src/models/group';
import { logger } from "matrix-js-sdk/src/logger";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import GroupStore from "../../../stores/GroupStore";
import { MenuItem } from "../../structures/ContextMenu";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.GroupInviteTileContextMenu")
export default class GroupInviteTileContextMenu extends React.Component {
static propTypes = {
group: PropTypes.instanceOf(Group).isRequired,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
};
constructor(props) {
super(props);
this._onClickReject = this._onClickReject.bind(this);
}
componentDidMount() {
this._unmounted = false;
}
componentWillUnmount() {
this._unmounted = true;
}
_onClickReject() {
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
title: _t('Reject invitation'),
description: _t('Are you sure you want to reject the invitation?'),
onFinished: async (shouldLeave) => {
if (!shouldLeave) return;
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
try {
await GroupStore.leaveGroup(this.props.group.groupId);
} catch (e) {
logger.error("Error rejecting community invite: ", e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
title: _t("Error"),
description: _t("Unable to reject invite"),
});
} finally {
modal.close();
}
},
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
}
render() {
return <div>
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg").default} width="15" height="15" alt="" />
{ _t('Reject') }
</MenuItem>
</div>;
}
}

View file

@ -1,112 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import { MenuItem } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
import { createSpaceFromCommunity } from "../../../utils/space";
import GroupStore from "../../../stores/GroupStore";
@replaceableComponent("views.context_menus.TagTileContextMenu")
export default class TagTileContextMenu extends React.Component {
static propTypes = {
tag: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func.isRequired,
};
static contextType = MatrixClientContext;
_onViewCommunityClick = () => {
dis.dispatch({
action: 'view_group',
group_id: this.props.tag,
});
this.props.onFinished();
};
_onRemoveClick = () => {
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
this.props.onFinished();
};
_onCreateSpaceClick = () => {
createSpaceFromCommunity(this.context, this.props.tag);
this.props.onFinished();
};
_onMoveUp = () => {
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
this.props.onFinished();
};
_onMoveDown = () => {
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
this.props.onFinished();
};
render() {
let moveUp;
let moveDown;
if (this.props.index > 0) {
moveUp = (
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveUp" onClick={this._onMoveUp}>
{ _t("Move up") }
</MenuItem>
);
}
if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
moveDown = (
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveDown" onClick={this._onMoveDown}>
{ _t("Move down") }
</MenuItem>
);
}
let createSpaceOption;
if (GroupStore.isUserPrivileged(this.props.tag)) {
createSpaceOption = <>
<hr className="mx_TagTileContextMenu_separator" />
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_createSpace" onClick={this._onCreateSpaceClick}>
{ _t("Create Space") }
</MenuItem>
</>;
}
return <div>
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
{ _t('View Community') }
</MenuItem>
{ (moveUp || moveDown) ? <hr className="mx_TagTileContextMenu_separator" /> : null }
{ moveUp }
{ moveDown }
<hr className="mx_TagTileContextMenu_separator" />
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
{ _t("Unpin") }
</MenuItem>
{ createSpaceOption }
</div>;
}
}

View file

@ -1,759 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018, 2019 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import { sleep } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { _t, _td } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { AddressType, addressTypes, getAddressType, IUserAddress } from '../../../UserAddress';
import GroupStore from '../../../stores/GroupStore';
import * as Email from '../../../email';
import IdentityAuthClient from '../../../IdentityAuthClient';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from '../../../utils/IdentityServerUtils';
import { abbreviateUrl } from '../../../utils/UrlUtils';
import { Action } from "../../../dispatcher/actions";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import AddressSelector from '../elements/AddressSelector';
import AddressTile from '../elements/AddressTile';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import AccessibleButton from '../elements/AccessibleButton';
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
const addressTypeName = {
'mx-user-id': _td("Matrix ID"),
'mx-room-id': _td("Matrix Room ID"),
'email': _td("email address"),
};
interface IResult {
user_id: string; // eslint-disable-line camelcase
room_id?: string; // eslint-disable-line camelcase
name?: string;
display_name?: string; // eslint-disable-line camelcase
avatar_url?: string;// eslint-disable-line camelcase
}
interface IProps {
title: string;
description?: JSX.Element;
// Extra node inserted after picker input, dropdown and errors
extraNode?: JSX.Element;
value?: string;
placeholder?: ((validAddressTypes: any) => string) | string;
roomId?: string;
button?: string;
focus?: boolean;
validAddressTypes?: AddressType[];
onFinished: (success: boolean, list?: IUserAddress[]) => void;
groupId?: string;
// The type of entity to search for. Default: 'user'.
pickerType?: 'user' | 'room';
// Whether the current user should be included in the addresses returned. Only
// applicable when pickerType is `user`. Default: false.
includeSelf?: boolean;
}
interface IState {
// Whether to show an error message because of an invalid address
invalidAddressError: boolean;
// List of UserAddressType objects representing
// the list of addresses we're going to invite
selectedList: IUserAddress[];
// Whether a search is ongoing
busy: boolean;
// An error message generated during the user directory search
searchError: string;
// Whether the server supports the user_directory API
serverSupportsUserDirectory: boolean;
// The query being searched for
query: string;
// List of UserAddressType objects representing the set of
// auto-completion results for the current search query.
suggestedList: IUserAddress[];
// List of address types initialised from props, but may change while the
// dialog is open and represents the supported list of address types at this time.
validAddressTypes: AddressType[];
}
@replaceableComponent("views.dialogs.AddressPickerDialog")
export default class AddressPickerDialog extends React.Component<IProps, IState> {
private textinput = createRef<HTMLTextAreaElement>();
private addressSelector = createRef<AddressSelector>();
private queryChangedDebouncer: number;
private cancelThreepidLookup: () => void;
static defaultProps: Partial<IProps> = {
value: "",
focus: true,
validAddressTypes: addressTypes,
pickerType: 'user',
includeSelf: false,
};
constructor(props: IProps) {
super(props);
let validAddressTypes = this.props.validAddressTypes;
// Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user
if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes(AddressType.Email)) {
validAddressTypes = validAddressTypes.filter(type => type !== AddressType.Email);
}
this.state = {
invalidAddressError: false,
selectedList: [],
busy: false,
searchError: null,
serverSupportsUserDirectory: true,
query: "",
suggestedList: [],
validAddressTypes,
};
}
componentDidMount() {
if (this.props.focus) {
// Set the cursor at the end of the text input
this.textinput.current.value = this.props.value;
}
}
private getPlaceholder(): string {
const { placeholder } = this.props;
if (typeof placeholder === "string") {
return placeholder;
}
// Otherwise it's a function, as checked by prop types.
return placeholder(this.state.validAddressTypes);
}
private onButtonClick = (): void => {
let selectedList = this.state.selectedList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local selectedList
if (this.textinput.current.value !== '') {
selectedList = this.addAddressesToList([this.textinput.current.value]);
if (selectedList === null) return;
}
this.props.onFinished(true, selectedList);
};
private onCancel = (): void => {
this.props.onFinished(false);
};
private onKeyDown = (e: React.KeyboardEvent): void => {
const textInput = this.textinput.current ? this.textinput.current.value : undefined;
let handled = true;
const action = getKeyBindingsManager().getAccessibilityAction(e);
if (action === KeyBindingAction.Escape) {
this.props.onFinished(false);
} else if (e.key === KeyBindingAction.ArrowUp) {
this.addressSelector.current?.moveSelectionUp();
} else if (e.key === KeyBindingAction.ArrowDown) {
this.addressSelector.current?.moveSelectionDown();
} else if (
[KeyBindingAction.Comma, KeyBindingAction.Enter, KeyBindingAction.Tab].includes(action) &&
this.state.suggestedList.length > 0
) {
this.addressSelector.current?.chooseSelection();
} else if (textInput.length === 0 && this.state.selectedList.length && action === KeyBindingAction.Backspace) {
this.onDismissed(this.state.selectedList.length - 1)();
} else if (e.key === KeyBindingAction.Enter) {
if (textInput === '') {
// if there's nothing in the input box, submit the form
this.onButtonClick();
} else {
this.addAddressesToList([textInput]);
}
} else if (textInput && [KeyBindingAction.Comma, KeyBindingAction.Tab].includes(action)) {
this.addAddressesToList([textInput]);
} else {
handled = false;
}
if (handled) {
e.stopPropagation();
e.preventDefault();
}
};
private onQueryChanged = (ev: React.ChangeEvent): void => {
const query = (ev.target as HTMLTextAreaElement).value;
if (this.queryChangedDebouncer) {
clearTimeout(this.queryChangedDebouncer);
}
// Only do search if there is something to search
if (query.length > 0 && query !== '@' && query.length >= 2) {
this.queryChangedDebouncer = setTimeout(() => {
if (this.props.pickerType === 'user') {
if (this.props.groupId) {
this.doNaiveGroupSearch(query);
} else if (this.state.serverSupportsUserDirectory) {
this.doUserDirectorySearch(query);
} else {
this.doLocalSearch(query);
}
} else if (this.props.pickerType === 'room') {
if (this.props.groupId) {
this.doNaiveGroupRoomSearch(query);
} else {
this.doRoomSearch(query);
}
} else {
logger.error('Unknown pickerType', this.props.pickerType);
}
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
} else {
this.setState({
suggestedList: [],
query: "",
searchError: null,
});
}
};
private onDismissed = (index: number) => () => {
const selectedList = this.state.selectedList.slice();
selectedList.splice(index, 1);
this.setState({
selectedList,
suggestedList: [],
query: "",
});
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
};
private onSelected = (index: number): void => {
const selectedList = this.state.selectedList.slice();
selectedList.push(this.getFilteredSuggestions()[index]);
this.setState({
selectedList,
suggestedList: [],
query: "",
});
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
};
private doNaiveGroupSearch(query: string): void {
const lowerCaseQuery = query.toLowerCase();
this.setState({
busy: true,
query,
searchError: null,
});
MatrixClientPeg.get().getGroupUsers(this.props.groupId).then((resp) => {
const results = [];
resp.chunk.forEach((u) => {
const userIdMatch = u.user_id.toLowerCase().includes(lowerCaseQuery);
const displayNameMatch = (u.displayname || '').toLowerCase().includes(lowerCaseQuery);
if (!(userIdMatch || displayNameMatch)) {
return;
}
results.push({
user_id: u.user_id,
avatar_url: u.avatar_url,
display_name: u.displayname,
});
});
this.processResults(results, query);
}).catch((err) => {
logger.error('Error whilst searching group rooms: ', err);
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
}).then(() => {
this.setState({
busy: false,
});
});
}
private doNaiveGroupRoomSearch(query: string): void {
const lowerCaseQuery = query.toLowerCase();
const results = [];
GroupStore.getGroupRooms(this.props.groupId).forEach((r) => {
const nameMatch = (r.name || '').toLowerCase().includes(lowerCaseQuery);
const topicMatch = (r.topic || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (r.canonical_alias || '').toLowerCase().includes(lowerCaseQuery);
if (!(nameMatch || topicMatch || aliasMatch)) {
return;
}
results.push({
room_id: r.room_id,
avatar_url: r.avatar_url,
name: r.name || r.canonical_alias,
});
});
this.processResults(results, query);
this.setState({
busy: false,
});
}
private doRoomSearch(query: string): void {
const lowerCaseQuery = query.toLowerCase();
const rooms = MatrixClientPeg.get().getRooms();
const results = [];
rooms.forEach((room) => {
let rank = Infinity;
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
const name = nameEvent ? nameEvent.getContent().name : '';
const canonicalAlias = room.getCanonicalAlias();
const aliasEvents = room.currentState.getStateEvents('m.room.aliases');
const aliases = aliasEvents.map((ev) => ev.getContent().aliases).reduce((a, b) => {
return a.concat(b);
}, []);
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
let aliasMatch = false;
let shortestMatchingAliasLength = Infinity;
aliases.forEach((alias) => {
if ((alias || '').toLowerCase().includes(lowerCaseQuery)) {
aliasMatch = true;
if (shortestMatchingAliasLength > alias.length) {
shortestMatchingAliasLength = alias.length;
}
}
});
if (!(nameMatch || aliasMatch)) {
return;
}
if (aliasMatch) {
// A shorter matching alias will give a better rank
rank = shortestMatchingAliasLength;
}
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
results.push({
rank,
room_id: room.roomId,
avatar_url: avatarUrl,
name: name || canonicalAlias || aliases[0] || _t('Unnamed Room'),
});
});
// Sort by rank ascending (a high rank being less relevant)
const sortedResults = results.sort((a, b) => {
return a.rank - b.rank;
});
this.processResults(sortedResults, query);
this.setState({
busy: false,
});
}
private doUserDirectorySearch(query: string): void {
this.setState({
busy: true,
query,
searchError: null,
});
MatrixClientPeg.get().searchUserDirectory({
term: query,
}).then((resp) => {
// The query might have changed since we sent the request, so ignore
// responses for anything other than the latest query.
if (this.state.query !== query) {
return;
}
this.processResults(resp.results, query);
}).catch((err) => {
logger.error('Error whilst searching user directory: ', err);
this.setState({
searchError: err.errcode ? err.message : _t('Something went wrong!'),
});
if (err.errcode === 'M_UNRECOGNIZED') {
this.setState({
serverSupportsUserDirectory: false,
});
// Do a local search immediately
this.doLocalSearch(query);
}
}).then(() => {
this.setState({
busy: false,
});
});
}
private doLocalSearch(query: string): void {
this.setState({
query,
searchError: null,
});
const queryLowercase = query.toLowerCase();
const results = [];
MatrixClientPeg.get().getUsers().forEach((user) => {
if (user.userId.toLowerCase().indexOf(queryLowercase) === -1 &&
user.displayName.toLowerCase().indexOf(queryLowercase) === -1
) {
return;
}
// Put results in the format of the new API
results.push({
user_id: user.userId,
display_name: user.displayName,
avatar_url: user.avatarUrl,
});
});
this.processResults(results, query);
}
private processResults(results: IResult[], query: string): void {
const suggestedList = [];
results.forEach((result) => {
if (result.room_id) {
const client = MatrixClientPeg.get();
const room = client.getRoom(result.room_id);
if (room) {
const tombstone = room.currentState.getStateEvents('m.room.tombstone', '');
if (tombstone && tombstone.getContent() && tombstone.getContent()["replacement_room"]) {
const replacementRoom = client.getRoom(tombstone.getContent()["replacement_room"]);
// Skip rooms with tombstones where we are also aware of the replacement room.
if (replacementRoom) return;
}
}
suggestedList.push({
addressType: 'mx-room-id',
address: result.room_id,
displayName: result.name,
avatarMxc: result.avatar_url,
isKnown: true,
});
return;
}
if (!this.props.includeSelf &&
result.user_id === MatrixClientPeg.get().credentials.userId
) {
return;
}
// Return objects, structure of which is defined
// by UserAddressType
suggestedList.push({
addressType: 'mx-user-id',
address: result.user_id,
displayName: result.display_name,
avatarMxc: result.avatar_url,
isKnown: true,
});
});
// If the query is a valid address, add an entry for that
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (this.state.validAddressTypes.includes(addrType)) {
if (addrType === 'email' && !Email.looksValid(query)) {
this.setState({ searchError: _t("That doesn't look like a valid email address") });
return;
}
suggestedList.unshift({
addressType: addrType,
address: query,
isKnown: false,
});
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
if (addrType === 'email') {
this.lookupThreepid(addrType, query);
}
}
this.setState({
suggestedList,
invalidAddressError: false,
}, () => {
if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop();
});
}
private addAddressesToList(addressTexts: string[]): IUserAddress[] {
const selectedList = this.state.selectedList.slice();
let hasError = false;
addressTexts.forEach((addressText) => {
addressText = addressText.trim();
const addrType = getAddressType(addressText);
const addrObj: IUserAddress = {
addressType: addrType,
address: addressText,
isKnown: false,
};
if (!this.state.validAddressTypes.includes(addrType)) {
hasError = true;
} else if (addrType === 'mx-user-id') {
const user = MatrixClientPeg.get().getUser(addrObj.address);
if (user) {
addrObj.displayName = user.displayName;
addrObj.avatarMxc = user.avatarUrl;
addrObj.isKnown = true;
}
} else if (addrType === 'mx-room-id') {
const room = MatrixClientPeg.get().getRoom(addrObj.address);
if (room) {
addrObj.displayName = room.name;
addrObj.isKnown = true;
}
}
selectedList.push(addrObj);
});
this.setState({
selectedList,
suggestedList: [],
query: "",
invalidAddressError: hasError ? true : this.state.invalidAddressError,
});
if (this.cancelThreepidLookup) this.cancelThreepidLookup();
return hasError ? null : selectedList;
}
private async lookupThreepid(medium: AddressType, address: string): Promise<string> {
let cancelled = false;
// Note that we can't safely remove this after we're done
// because we don't know that it's the same one, so we just
// leave it: it's replacing the old one each time so it's
// not like they leak.
this.cancelThreepidLookup = function() {
cancelled = true;
};
// wait a bit to let the user finish typing
await sleep(500);
if (cancelled) return null;
try {
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
if (cancelled) return null;
const lookup = await MatrixClientPeg.get().lookupThreePid(
medium,
address,
undefined /* callback */,
identityAccessToken,
);
if (cancelled || lookup === null || !lookup.mxid) return null;
const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid);
if (cancelled || profile === null) return null;
this.setState({
suggestedList: [{
// a UserAddressType
addressType: medium,
address: address,
displayName: profile.displayname,
avatarMxc: profile.avatar_url,
isKnown: true,
}],
});
} catch (e) {
logger.error(e);
this.setState({
searchError: _t('Something went wrong!'),
});
}
}
private getFilteredSuggestions(): IUserAddress[] {
// map addressType => set of addresses to avoid O(n*m) operation
const selectedAddresses = {};
this.state.selectedList.forEach(({ address, addressType }) => {
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
selectedAddresses[addressType].add(address);
});
// Filter out any addresses in the above already selected addresses (matching both type and address)
return this.state.suggestedList.filter(({ address, addressType }) => {
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
});
}
private onPaste = (e: React.ClipboardEvent): void => {
// Prevent the text being pasted into the textarea
e.preventDefault();
const text = e.clipboardData.getData("text");
// Process it as a list of addresses to add instead
this.addAddressesToList(text.split(/[\s,]+/));
};
private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => {
e.preventDefault();
// Update the IS in account data. Actually using it may trigger terms.
// eslint-disable-next-line react-hooks/rules-of-hooks
useDefaultIdentityServer();
// Add email as a valid address type.
const { validAddressTypes } = this.state;
validAddressTypes.push(AddressType.Email);
this.setState({ validAddressTypes });
};
private onManageSettingsClick = (e: React.MouseEvent): void => {
e.preventDefault();
dis.fire(Action.ViewUserSettings);
this.onCancel();
};
render() {
let inputLabel;
if (this.props.description) {
inputLabel = <div className="mx_AddressPickerDialog_label">
<label htmlFor="textinput">{ this.props.description }</label>
</div>;
}
const query = [];
// create the invite list
if (this.state.selectedList.length > 0) {
for (let i = 0; i < this.state.selectedList.length; i++) {
query.push(
<AddressTile
key={i}
address={this.state.selectedList[i]}
canDismiss={true}
onDismissed={this.onDismissed(i)}
showAddress={this.props.pickerType === 'user'} />,
);
}
}
// Add the query at the end
query.push(
<textarea
key={this.state.selectedList.length}
onPaste={this.onPaste}
rows={1}
id="textinput"
ref={this.textinput}
className="mx_AddressPickerDialog_input"
onChange={this.onQueryChanged}
placeholder={this.getPlaceholder()}
defaultValue={this.props.value}
autoFocus={this.props.focus}
/>,
);
const filteredSuggestedList = this.getFilteredSuggestions();
let error;
let addressSelector;
if (this.state.invalidAddressError) {
const validTypeDescriptions = this.state.validAddressTypes.map((t) => _t(addressTypeName[t]));
error = <div className="mx_AddressPickerDialog_error">
{ _t("You have entered an invalid address.") }
<br />
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
validTypesList: validTypeDescriptions.join(", "),
}) }
</div>;
} else if (this.state.searchError) {
error = <div className="mx_AddressPickerDialog_error">{ this.state.searchError }</div>;
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
error = <div className="mx_AddressPickerDialog_error">{ _t("No results") }</div>;
} else {
addressSelector = (
<AddressSelector ref={this.addressSelector}
addressList={filteredSuggestedList}
showAddress={this.props.pickerType === 'user'}
onSelected={this.onSelected}
truncateAt={TRUNCATE_QUERY_LIST}
/>
);
}
let identityServer;
// If picker cannot currently accept e-mail but should be able to
if (this.props.pickerType === 'user' && !this.state.validAddressTypes.includes(AddressType.Email)
&& this.props.validAddressTypes.includes(AddressType.Email)) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) {
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
"Use an identity server to invite by email. " +
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
"or manage in <settings>Settings</settings>.",
{
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
},
{
default: sub => (
<AccessibleButton kind="link_inline" onClick={this.onUseDefaultIdentityServerClick}>
{ sub }
</AccessibleButton>
),
settings: sub => <AccessibleButton kind="link_inline" onClick={this.onManageSettingsClick}>
{ sub }
</AccessibleButton>,
},
) }</div>;
} else {
identityServer = <div className="mx_AddressPickerDialog_identityServer">{ _t(
"Use an identity server to invite by email. " +
"Manage in <settings>Settings</settings>.",
{}, {
settings: sub => <AccessibleButton kind="link_inline" onClick={this.onManageSettingsClick}>
{ sub }
</AccessibleButton>,
},
) }</div>;
}
}
return (
<BaseDialog
className="mx_AddressPickerDialog"
onKeyDown={this.onKeyDown}
onFinished={this.props.onFinished}
title={this.props.title}
>
{ inputLabel }
<div className="mx_Dialog_content">
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
{ error }
{ addressSelector }
{ this.props.extraNode }
{ identityServer }
</div>
<DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onButtonClick}
onCancel={this.onCancel} />
</BaseDialog>
);
}
}

View file

@ -204,7 +204,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
<p>
{ _t(
"Debug logs contain application usage data including your " +
"username, the IDs or aliases of the rooms or groups you " +
"username, the IDs or aliases of the rooms you " +
"have visited, which UI elements you last interacted with, " +
"and the usernames of other users. They do not contain messages.",
) }

View file

@ -1,260 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent, FormEvent } from 'react';
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { logger } from "matrix-js-sdk/src/logger";
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { arrayFastClone } from "../../../utils/arrays";
import SdkConfig from "../../../SdkConfig";
import InviteDialog from "./InviteDialog";
import BaseAvatar from "../avatars/BaseAvatar";
import { inviteMultipleToRoom, showAnyInviteErrors } from "../../../RoomInvite";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends IDialogProps {
roomId: string;
communityName: string;
}
interface IPerson {
userId: string;
user: RoomMember;
lastActive: number;
}
interface IState {
emailTargets: string[];
userTargets: string[];
showPeople: boolean;
people: IPerson[];
numPeople: number;
busy: boolean;
}
@replaceableComponent("views.dialogs.CommunityPrototypeInviteDialog")
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
emailTargets: [],
userTargets: [],
showPeople: false,
people: this.buildSuggestions(),
numPeople: 5, // arbitrary default
busy: false,
};
}
private buildSuggestions(): IPerson[] {
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get("welcome_user_id")]);
if (this.props.roomId) {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
// add banned users, so we don't try to invite them
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
}
return InviteDialog.buildRecents(alreadyInvited);
}
private onSubmit = async (ev: FormEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({ busy: true });
try {
const targets = [...this.state.emailTargets, ...this.state.userTargets];
const result = await inviteMultipleToRoom(this.props.roomId, targets);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const success = showAnyInviteErrors(result.states, room, result.inviter);
if (success) {
this.props.onFinished(true);
} else {
this.setState({ busy: false });
}
} catch (e) {
this.setState({ busy: false });
logger.error(e);
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
description: ((e && e.message) ? e.message : _t("Operation failed")),
});
}
};
private onAddressChange = (ev: ChangeEvent<HTMLInputElement>, index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) {
targets.push(ev.target.value);
} else {
targets[index] = ev.target.value;
}
this.setState({ emailTargets: targets });
};
private onAddressBlur = (index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) return; // not important
if (targets[index].trim() === "") {
targets.splice(index, 1);
this.setState({ emailTargets: targets });
}
};
private onShowPeopleClick = () => {
this.setState({ showPeople: !this.state.showPeople });
};
private setPersonToggle = (person: IPerson, selected: boolean) => {
const targets = arrayFastClone(this.state.userTargets);
if (selected && !targets.includes(person.userId)) {
targets.push(person.userId);
} else if (!selected && targets.includes(person.userId)) {
targets.splice(targets.indexOf(person.userId), 1);
}
this.setState({ userTargets: targets });
};
private renderPerson(person: IPerson, key: any) {
const avatarSize = 36;
let avatarUrl = null;
if (person.user.getMxcAvatarUrl()) {
avatarUrl = mediaFromMxc(person.user.getMxcAvatarUrl()).getSquareThumbnailHttp(avatarSize);
}
return (
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
<BaseAvatar
url={avatarUrl}
name={person.user.name}
idName={person.user.userId}
width={avatarSize}
height={avatarSize}
/>
<div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
<span className="mx_CommunityPrototypeInviteDialog_personName">{ person.user.name }</span>
<span className="mx_CommunityPrototypeInviteDialog_personId">{ person.userId }</span>
</div>
<StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
</div>
);
}
private onShowMorePeople = () => {
this.setState({ numPeople: this.state.numPeople + 5 }); // arbitrary increase
};
public render() {
const emailAddresses = [];
this.state.emailTargets.forEach((address, i) => {
emailAddresses.push((
<Field
key={i}
value={address}
onChange={(e) => this.onAddressChange(e, i)}
label={_t("Email address")}
placeholder={_t("Email address")}
onBlur={() => this.onAddressBlur(i)}
/>
));
});
// Push a clean input
emailAddresses.push((
<Field
key={emailAddresses.length}
value=""
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
/>
));
let peopleIntro = null;
const people = [];
if (this.state.showPeople) {
const humansToPresent = this.state.people.slice(0, this.state.numPeople);
humansToPresent.forEach((person, i) => {
people.push(this.renderPerson(person, i));
});
if (humansToPresent.length < this.state.people.length) {
people.push((
<AccessibleButton
onClick={this.onShowMorePeople}
kind="link"
key="more"
className="mx_CommunityPrototypeInviteDialog_morePeople"
>
{ _t("Show more") }
</AccessibleButton>
));
}
}
if (this.state.people.length > 0) {
peopleIntro = (
<div className="mx_CommunityPrototypeInviteDialog_people">
<span>{ _t("People you know on %(brand)s", { brand: SdkConfig.get().brand }) }</span>
<AccessibleButton onClick={this.onShowPeopleClick}>
{ this.state.showPeople ? _t("Hide") : _t("Show") }
</AccessibleButton>
</div>
);
}
let buttonText = _t("Skip");
const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
if (targetCount > 0) {
buttonText = _t("Send %(count)s invites", { count: targetCount });
}
return (
<BaseDialog
className="mx_CommunityPrototypeInviteDialog"
onFinished={this.props.onFinished}
title={_t("Invite people to join %(communityName)s", { communityName: this.props.communityName })}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
{ emailAddresses }
{ peopleIntro }
{ people }
<AccessibleButton
kind="primary"
onClick={this.onSubmit}
disabled={this.state.busy}
className="mx_CommunityPrototypeInviteDialog_primaryButton"
>
{ buttonText }
</AccessibleButton>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -22,7 +22,7 @@ import SpaceStore from "../../../stores/spaces/SpaceStore";
import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker";
type BaseProps = ComponentProps<typeof ConfirmUserActionDialog>;
interface IProps extends Omit<BaseProps, "groupMember" | "matrixClient" | "children" | "onFinished"> {
interface IProps extends Omit<BaseProps, "matrixClient" | "children" | "onFinished"> {
space: Room;
allLabel: string;
specificLabel: string;

View file

@ -15,28 +15,20 @@ limitations under the License.
*/
import React, { ChangeEvent, FormEvent, ReactNode } from 'react';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import MemberAvatar from '../avatars/MemberAvatar';
import BaseAvatar from '../avatars/BaseAvatar';
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import Field from '../elements/Field';
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
interface IProps {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member?: RoomMember;
// group member object. Supply either this or 'member'
groupMember?: GroupMemberType;
// needed if a group member is specified
matrixClient?: MatrixClient;
// matrix-js-sdk (room) member object.
member: RoomMember;
action: string; // eg. 'Ban'
title: string; // eg. 'Ban this user?'
@ -112,21 +104,9 @@ export default class ConfirmUserActionDialog extends React.Component<IProps, ISt
);
}
let avatar;
let name;
let userId;
if (this.props.member) {
avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
name = this.props.member.name;
userId = this.props.member.userId;
} else {
const httpAvatarUrl = this.props.groupMember.avatarUrl
? mediaFromMxc(this.props.groupMember.avatarUrl).getSquareThumbnailHttp(48)
: null;
name = this.props.groupMember.displayname || this.props.groupMember.userId;
userId = this.props.groupMember.userId;
avatar = <BaseAvatar name={name} url={httpAvatarUrl} width={48} height={48} />;
}
const avatar = <MemberAvatar member={this.props.member} width={48} height={48} />;
const name = this.props.member.name;
const userId = this.props.member.userId;
const displayUserIdentifier = UserIdentifierCustomisations.getDisplayUserIdentifier(
userId, { roomId: this.props.roomId, withDisplayName: true },

View file

@ -1,236 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent } from 'react';
import { logger } from "matrix-js-sdk/src/logger";
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import InfoTooltip from "../elements/InfoTooltip";
import dis from "../../../dispatcher/dispatcher";
import { Action } from '../../../dispatcher/actions';
import { showCommunityRoomInviteDialog } from "../../../RoomInvite";
import GroupStore from "../../../stores/GroupStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
interface IProps extends IDialogProps {
}
interface IState {
name: string;
localpart: string;
error: string;
busy: boolean;
avatarFile: File;
avatarPreview: string;
}
@replaceableComponent("views.dialogs.CreateCommunityPrototypeDialog")
export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
constructor(props: IProps) {
super(props);
this.state = {
name: "",
localpart: "",
error: null,
busy: false,
avatarFile: null,
avatarPreview: null,
};
}
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
this.setState({ name: ev.target.value, localpart });
};
private onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (this.state.busy) return;
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({ busy: true });
try {
let avatarUrl = ''; // must be a string for synapse to accept it
if (this.state.avatarFile) {
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
}
const result = await MatrixClientPeg.get().createGroup({
localpart: this.state.localpart,
profile: {
name: this.state.name,
avatar_url: avatarUrl,
},
});
// Ensure the tag gets selected now that we've created it
dis.dispatch({ action: 'deselect_tags' }, true);
dis.dispatch({
action: 'select_tag',
tag: result.group_id,
});
// Close our own dialog before moving much further
this.props.onFinished(true);
if (result.room_id) {
// Force the group store to update as it might have missed the general chat
await GroupStore.refreshGroupRooms(result.group_id);
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: result.room_id,
metricsTrigger: undefined, // Deprecated groups
});
showCommunityRoomInviteDialog(result.room_id, this.state.name);
} else {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
}
} catch (e) {
logger.error(e);
this.setState({
busy: false,
error: _t(
"There was an error creating your community. The name may be taken or the " +
"server is unable to process your request.",
),
});
}
};
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({ avatarFile: null });
} else {
this.setState({ busy: true });
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({ avatarFile: file, busy: false, avatarPreview: ev.target.result as string });
};
reader.readAsDataURL(file);
}
};
private onChangeAvatar = () => {
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
};
public render() {
let communityId = null;
if (this.state.localpart) {
communityId = (
<span className="mx_CreateCommunityPrototypeDialog_communityId">
{ _t("Community ID: +<localpart />:%(domain)s", {
domain: MatrixClientPeg.getHomeserverName(),
}, {
localpart: () => <u>{ this.state.localpart }</u>,
}) }
<InfoTooltip
tooltip={_t(
"Use this when referencing your community to others. The community ID " +
"cannot be changed.",
)}
/>
</span>
);
}
let helpText = (
<span className="mx_CreateCommunityPrototypeDialog_subtext">
{ _t("You can change this later if needed.") }
</span>
);
if (this.state.error) {
const classes = "mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error";
helpText = (
<span className={classes}>
{ this.state.error }
</span>
);
}
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />;
}
return (
<BaseDialog
className="mx_CreateCommunityPrototypeDialog"
onFinished={this.props.onFinished}
title={_t("What's the name of your community or team?")}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateCommunityPrototypeDialog_colName">
<Field
value={this.state.name}
onChange={this.onNameChange}
placeholder={_t("Enter name")}
label={_t("Enter name")}
/>
{ helpText }
<span className="mx_CreateCommunityPrototypeDialog_subtext">
{ /*nbsp is to reserve the height of this element when there's nothing*/ }
&nbsp;{ communityId }
</span>
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
{ _t("Create") }
</AccessibleButton>
</div>
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
<input
type="file"
style={{ display: "none" }}
ref={this.avatarUploadRef}
accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton
onClick={this.onChangeAvatar}
className="mx_CreateCommunityPrototypeDialog_avatarContainer"
>
{ preview }
</AccessibleButton>
<div className="mx_CreateCommunityPrototypeDialog_tip">
<b>{ _t("Add image (optional)") }</b>
<span>
{ _t("An image will help people identify your community.") }
</span>
</div>
</div>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -1,184 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BaseDialog from "./BaseDialog";
import Spinner from "../elements/Spinner";
interface IProps {
onFinished: (success: boolean) => void;
}
interface IState {
groupName: string;
groupId: string;
groupIdError: string;
creating: boolean;
createError: Error;
}
@replaceableComponent("views.dialogs.CreateGroupDialog")
export default class CreateGroupDialog extends React.Component<IProps, IState> {
public state = {
groupName: '',
groupId: '',
groupIdError: '',
creating: false,
createError: null,
};
private onGroupNameChange = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({
groupName: e.currentTarget.value,
});
};
private onGroupIdChange = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({
groupId: e.currentTarget.value,
});
};
private onGroupIdBlur = (): void => {
this.checkGroupId();
};
private checkGroupId() {
let error = null;
if (!this.state.groupId) {
error = _t("Community IDs cannot be empty.");
} else if (!/^[a-z0-9=_\-./]*$/.test(this.state.groupId)) {
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
}
this.setState({
groupIdError: error,
// Reset createError to get rid of now stale error message
createError: null,
});
return error;
}
private onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (this.checkGroupId()) return;
const profile: any = {};
if (this.state.groupName !== '') {
profile.name = this.state.groupName;
}
this.setState({ creating: true });
MatrixClientPeg.get().createGroup({
localpart: this.state.groupId,
profile: profile,
}).then((result) => {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
this.props.onFinished(true);
}).catch((e) => {
this.setState({ createError: e });
}).finally(() => {
this.setState({ creating: false });
});
};
private onCancel = () => {
this.props.onFinished(false);
};
render() {
if (this.state.creating) {
return <Spinner />;
}
let createErrorNode;
if (this.state.createError) {
// XXX: We should catch errcodes and give sensible i18ned messages for them,
// rather than displaying what the server gives us, but synapse doesn't give
// any yet.
createErrorNode = <div className="error" role="alert">
<div>{ _t('Something went wrong whilst creating your community') }</div>
<div>{ this.state.createError.message }</div>
</div>;
}
return (
<BaseDialog
className="mx_CreateGroupDialog"
onFinished={this.props.onFinished}
title={_t('Create Community')}
>
<form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
<label htmlFor="groupname">{ _t('Community Name') }</label>
</div>
<div>
<input
id="groupname"
className="mx_CreateGroupDialog_input"
autoFocus={true}
size={64}
placeholder={_t('Example')}
onChange={this.onGroupNameChange}
value={this.state.groupName}
/>
</div>
</div>
<div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label">
<label htmlFor="groupid">{ _t('Community ID') }</label>
</div>
<div className="mx_CreateGroupDialog_input_group">
<span className="mx_CreateGroupDialog_prefix">+</span>
<input id="groupid"
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
size={32}
placeholder={_t('example')}
onChange={this.onGroupIdChange}
onBlur={this.onGroupIdBlur}
value={this.state.groupId}
/>
<span className="mx_CreateGroupDialog_suffix">
:{ MatrixClientPeg.get().getDomain() }
</span>
</div>
</div>
<div className="error">
{ this.state.groupIdError }
</div>
{ createErrorNode }
</div>
<div className="mx_Dialog_buttons">
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -26,7 +26,6 @@ import withValidation, { IFieldState } from '../elements/Validation';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Heading from "../typography/Heading";
import Field from "../elements/Field";
@ -122,10 +121,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
createOpts.creation_content = { 'm.federate': false };
}
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
}
opts.parentSpace = this.props.parentSpace;
if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) {
opts.joinRule = JoinRule.Restricted;
@ -250,14 +245,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
}
let publicPrivateLabel: JSX.Element;
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
publicPrivateLabel = <p>
{ _t(
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone in this community.",
) }
</p>;
} else if (this.state.joinRule === JoinRule.Restricted) {
if (this.state.joinRule === JoinRule.Restricted) {
publicPrivateLabel = <p>
{ _t(
"Everyone in <SpaceName/> will be able to find and join this room.", {}, {
@ -332,10 +320,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
}
let title = _t("Create a room");
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
title = _t("Create a room in %(communityName)s", { communityName: name });
} else if (!this.props.parentSpace) {
if (!this.props.parentSpace) {
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
}

View file

@ -1,353 +0,0 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect, useRef, useState } from "react";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from '../../../languageHandler';
import BaseDialog from "./BaseDialog";
import AccessibleButton from "../elements/AccessibleButton";
import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu";
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
import Field from "../elements/Field";
import RoomAliasField from "../elements/RoomAliasField";
import { GroupMember } from "../right_panel/UserInfo";
import { parseMembersResponse, parseRoomsResponse } from "../../../stores/GroupStore";
import { calculateRoomVia, makeRoomPermalink } from "../../../utils/permalinks/Permalinks";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import Spinner from "../elements/Spinner";
import { mediaFromMxc } from "../../../customisations/Media";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserSettingsDialog";
import TagOrderActions from "../../../actions/TagOrderActions";
import { inviteUsersToRoom } from "../../../RoomInvite";
import ProgressBar from "../elements/ProgressBar";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { CreateEventField, IGroupRoom, IGroupSummary } from "../../../@types/groups";
interface IProps {
matrixClient: MatrixClient;
groupId: string;
onFinished(spaceId?: string): void;
}
enum Progress {
NotStarted,
ValidatingInputs,
FetchingData,
CreatingSpace,
InvitingUsers,
// anything beyond here is inviting user n - 4
}
const CreateSpaceFromCommunityDialog: React.FC<IProps> = ({ matrixClient: cli, groupId, onFinished }) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>(null);
const [progress, setProgress] = useState(Progress.NotStarted);
const [numInvites, setNumInvites] = useState(0);
const busy = progress > 0;
const [avatar, setAvatar] = useState<File>(null); // undefined means to remove avatar
const [name, setName] = useState("");
const spaceNameField = useRef<Field>();
const [alias, setAlias] = useState("#" + groupId.substring(1, groupId.indexOf(":")) + ":" + cli.getDomain());
const spaceAliasField = useRef<RoomAliasField>();
const [topic, setTopic] = useState("");
const [joinRule, setJoinRule] = useState<JoinRule>(JoinRule.Public);
const groupSummary = useAsyncMemo<IGroupSummary>(() => cli.getGroupSummary(groupId), [groupId]);
useEffect(() => {
if (groupSummary) {
setName(groupSummary.profile.name || "");
setTopic(groupSummary.profile.short_description || "");
setJoinRule(groupSummary.profile.is_openly_joinable ? JoinRule.Public : JoinRule.Invite);
setLoading(false);
}
}, [groupSummary]);
if (loading) {
return <Spinner />;
}
const onCreateSpaceClick = async (e) => {
e.preventDefault();
if (busy) return;
setError(null);
setProgress(Progress.ValidatingInputs);
// require & validate the space name field
if (!(await spaceNameField.current.validate({ allowEmpty: false }))) {
setProgress(0);
spaceNameField.current.focus();
spaceNameField.current.validate({ allowEmpty: false, focused: true });
return;
}
// validate the space name alias field but do not require it
if (joinRule === JoinRule.Public && !(await spaceAliasField.current.validate({ allowEmpty: true }))) {
setProgress(0);
spaceAliasField.current.focus();
spaceAliasField.current.validate({ allowEmpty: true, focused: true });
return;
}
try {
setProgress(Progress.FetchingData);
const [rooms, members, invitedMembers] = await Promise.all([
cli.getGroupRooms(groupId).then(parseRoomsResponse) as Promise<IGroupRoom[]>,
cli.getGroupUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
cli.getGroupInvitedUsers(groupId).then(parseMembersResponse) as Promise<GroupMember[]>,
]);
setNumInvites(members.length + invitedMembers.length);
const viaMap = new Map<string, string[]>();
for (const { roomId, canonicalAlias } of rooms) {
const room = cli.getRoom(roomId);
if (room) {
viaMap.set(roomId, calculateRoomVia(room));
} else if (canonicalAlias) {
try {
const { servers } = await cli.getRoomIdForAlias(canonicalAlias);
viaMap.set(roomId, servers);
} catch (e) {
logger.warn("Failed to resolve alias during community migration", e);
}
}
if (!viaMap.get(roomId)?.length) {
// XXX: lets guess the via, this might end up being incorrect.
const str = canonicalAlias || roomId;
viaMap.set(roomId, [str.substring(1, str.indexOf(":"))]);
}
}
setProgress(Progress.CreatingSpace);
const spaceAvatar = avatar !== undefined ? avatar : groupSummary.profile.avatar_url;
const roomId = await createSpace(name, joinRule === JoinRule.Public, alias, topic, spaceAvatar, {
creation_content: {
[CreateEventField]: groupId,
},
initial_state: rooms.map(({ roomId }) => ({
type: EventType.SpaceChild,
state_key: roomId,
content: {
via: viaMap.get(roomId) || [],
},
})),
// we do not specify the inviters here because Synapse applies a limit and this may cause it to trip
}, {
andView: false,
});
setProgress(Progress.InvitingUsers);
const userIds = [...members, ...invitedMembers].map(m => m.userId).filter(m => m !== cli.getUserId());
await inviteUsersToRoom(roomId, userIds, false, () => setProgress(p => p + 1));
// eagerly remove it from the community panel
dis.dispatch(TagOrderActions.removeTag(cli, groupId));
// don't bother awaiting this, as we don't hugely care if it fails
cli.setGroupProfile(groupId, {
...groupSummary.profile,
long_description: `<a href="${makeRoomPermalink(roomId)}"><h1>` +
_t("This community has been upgraded into a Space") + `</h1></a><br />`
+ groupSummary.profile.long_description,
} as IGroupSummary["profile"]).catch(e => {
logger.warn("Failed to update community profile during migration", e);
});
onFinished(roomId);
const onSpaceClick = () => {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
metricsTrigger: undefined, // other
});
};
const onPreferencesClick = () => {
dis.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Preferences,
});
};
let spacesDisabledCopy;
if (!SpaceStore.spacesEnabled) {
spacesDisabledCopy = _t("To view Spaces, hide communities in <a>Preferences</a>", {}, {
a: sub => <AccessibleButton onClick={onPreferencesClick} kind="link">{ sub }</AccessibleButton>,
});
}
Modal.createDialog(InfoDialog, {
title: _t("Space created"),
description: <>
<div className="mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog_checkmark" />
<p>
{ _t("<SpaceName/> has been made and everyone who was a part of the community has " +
"been invited to it.", {}, {
SpaceName: () => <AccessibleButton onClick={onSpaceClick} kind="link">
{ name }
</AccessibleButton>,
}) }
&nbsp;
{ spacesDisabledCopy }
</p>
<p>
{ _t("To create a Space from another community, just pick the community in Preferences.") }
</p>
</>,
button: _t("Preferences"),
onFinished: (openPreferences: boolean) => {
if (openPreferences) {
onPreferencesClick();
}
},
}, "mx_CreateSpaceFromCommunityDialog_SuccessInfoDialog");
} catch (e) {
logger.error(e);
setError(e);
}
setProgress(Progress.NotStarted);
};
let footer;
if (error) {
footer = <>
<img src={require("../../../../res/img/element-icons/warning-badge.svg").default} height="24" width="24" alt="" />
<span className="mx_CreateSpaceFromCommunityDialog_error">
<div className="mx_CreateSpaceFromCommunityDialog_errorHeading">{ _t("Failed to migrate community") }</div>
<div className="mx_CreateSpaceFromCommunityDialog_errorCaption">{ _t("Try again") }</div>
</span>
<AccessibleButton className="mx_CreateSpaceFromCommunityDialog_retryButton" onClick={onCreateSpaceClick}>
{ _t("Retry") }
</AccessibleButton>
</>;
} else if (busy) {
let description: string;
switch (progress) {
case Progress.ValidatingInputs:
case Progress.FetchingData:
description = _t("Fetching data...");
break;
case Progress.CreatingSpace:
description = _t("Creating Space...");
break;
case Progress.InvitingUsers:
default:
description = _t("Adding rooms... (%(progress)s out of %(count)s)", {
count: numInvites,
progress,
});
break;
}
footer = <span>
<ProgressBar
value={progress > Progress.FetchingData ? progress : 0}
max={numInvites + Progress.InvitingUsers}
/>
<div className="mx_CreateSpaceFromCommunityDialog_progressText">
{ description }
</div>
</span>;
} else {
footer = <>
<AccessibleButton kind="primary_outline" onClick={() => onFinished()}>
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={onCreateSpaceClick}>
{ _t("Create Space") }
</AccessibleButton>
</>;
}
return <BaseDialog
title={_t("Create Space from community")}
className="mx_CreateSpaceFromCommunityDialog"
onFinished={onFinished}
fixedWidth={false}
>
<div className="mx_CreateSpaceFromCommunityDialog_content">
<p>
{ _t("A link to the Space will be put in your community description.") }
&nbsp;
{ _t("All rooms will be added and all community members will be invited.") }
</p>
<p className="mx_CreateSpaceFromCommunityDialog_flairNotice">
{ _t("Flair won't be available in Spaces for the foreseeable future.") }
</p>
<SpaceCreateForm
busy={busy}
onSubmit={onCreateSpaceClick}
avatarUrl={groupSummary.profile.avatar_url
? mediaFromMxc(groupSummary.profile.avatar_url).getThumbnailOfSourceHttp(80, 80, "crop")
: undefined
}
setAvatar={setAvatar}
name={name}
setName={setName}
nameFieldRef={spaceNameField}
topic={topic}
setTopic={setTopic}
alias={alias}
setAlias={setAlias}
showAliasField={joinRule === JoinRule.Public}
aliasFieldRef={spaceAliasField}
>
<p>{ _t("This description will be shown to people when they view your space") }</p>
<JoinRuleDropdown
label={_t("Space visibility")}
labelInvite={_t("Private space (invite only)")}
labelPublic={_t("Public space")}
value={joinRule}
onChange={setJoinRule}
/>
<p>{ joinRule === JoinRule.Public
? _t("Open space for anyone, best for communities")
: _t("Invite only, best for yourself or teams")
}</p>
{ joinRule !== JoinRule.Public &&
<div className="mx_CreateSpaceFromCommunityDialog_nonPublicSpacer" />
}
</SpaceCreateForm>
</div>
<div className="mx_CreateSpaceFromCommunityDialog_footer">
{ footer }
</div>
</BaseDialog>;
};
export default CreateSpaceFromCommunityDialog;

View file

@ -1,174 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ChangeEvent } from 'react';
import { logger } from "matrix-js-sdk/src/logger";
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import FlairStore from "../../../stores/FlairStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends IDialogProps {
communityId: string;
}
interface IState {
name: string;
error: string;
busy: boolean;
currentAvatarUrl: string;
avatarFile: File;
avatarPreview: string;
}
// XXX: This is a lot of duplication from the create dialog, just in a different shape
@replaceableComponent("views.dialogs.EditCommunityPrototypeDialog")
export default class EditCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
constructor(props: IProps) {
super(props);
const profile = CommunityPrototypeStore.instance.getCommunityProfile(props.communityId);
this.state = {
name: profile?.name || "",
error: null,
busy: false,
avatarFile: null,
avatarPreview: null,
currentAvatarUrl: profile?.avatarUrl,
};
}
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({ name: ev.target.value });
};
private onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (this.state.busy) return;
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({ busy: true });
try {
let avatarUrl = this.state.currentAvatarUrl || ""; // must be a string for synapse to accept it
if (this.state.avatarFile) {
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
}
await MatrixClientPeg.get().setGroupProfile(this.props.communityId, {
name: this.state.name,
avatar_url: avatarUrl,
});
// ask the flair store to update the profile too
await FlairStore.refreshGroupProfile(MatrixClientPeg.get(), this.props.communityId);
// we did it, so close the dialog
this.props.onFinished(true);
} catch (e) {
logger.error(e);
this.setState({
busy: false,
error: _t("There was an error updating your community. The server is unable to process your request."),
});
}
};
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({ avatarFile: null });
} else {
this.setState({ busy: true });
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({ avatarFile: file, busy: false, avatarPreview: ev.target.result as string });
};
reader.readAsDataURL(file);
}
};
private onChangeAvatar = () => {
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
};
public render() {
let preview = <img src={this.state.avatarPreview} className="mx_EditCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
if (this.state.currentAvatarUrl) {
const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
} else {
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />;
}
}
return (
<BaseDialog
className="mx_EditCommunityPrototypeDialog"
onFinished={this.props.onFinished}
title={_t("Update community")}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
<div className="mx_EditCommunityPrototypeDialog_rowName">
<Field
value={this.state.name}
onChange={this.onNameChange}
placeholder={_t("Enter name")}
label={_t("Enter name")}
/>
</div>
<div className="mx_EditCommunityPrototypeDialog_rowAvatar">
<input
type="file"
style={{ display: "none" }}
ref={this.avatarUploadRef}
accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton
onClick={this.onChangeAvatar}
className="mx_EditCommunityPrototypeDialog_avatarContainer"
>{ preview }</AccessibleButton>
<div className="mx_EditCommunityPrototypeDialog_tip">
<b>{ _t("Add image (optional)") }</b>
<span>
{ _t("An image will help people identify your community.") }
</span>
</div>
</div>
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
{ _t("Save") }
</AccessibleButton>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -23,8 +23,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher";
import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
import { UIFeature } from "../../../settings/UIFeature";
import { useSettingValue } from "../../../hooks/useSettings";
import { Layout } from "../../../settings/enums/Layout";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
@ -43,7 +42,6 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { roomContextDetailsText } from "../../../Rooms";
import { Action } from "../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
@ -190,16 +188,13 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
const spacesEnabled = SpaceStore.spacesEnabled;
const flairEnabled = useFeatureEnabled(UIFeature.Flair);
const previewLayout = useSettingValue<Layout>("layout");
let rooms = useMemo(() => sortRooms(
cli.getVisibleRooms().filter(
room => room.getMyMembership() === "join" &&
!(spacesEnabled && room.isSpaceRoom()),
room => room.getMyMembership() === "join" && !room.isSpaceRoom(),
),
), [cli, spacesEnabled]);
), [cli]);
if (lcQuery) {
rooms = new QueryMatcher<Room>(rooms, {
@ -241,7 +236,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
<EventTile
mxEvent={mockEvent}
layout={previewLayout}
enableFlair={flairEnabled}
permalinkCreator={permalinkCreator}
as="div"
/>

View file

@ -43,12 +43,10 @@ import {
IInviteResult,
inviteMultipleToRoom,
showAnyInviteErrors,
showCommunityInviteDialog,
} from "../../../RoomInvite";
import { Action } from "../../../dispatcher/actions";
import { DefaultTagID } from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -64,7 +62,6 @@ import QuestionDialog from "./QuestionDialog";
import Spinner from "../elements/Spinner";
import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import CallHandler from "../../../CallHandler";
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
import CopyableText from "../elements/CopyableText";
@ -1104,23 +1101,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
this.props.onFinished(false);
};
private onCommunityInviteClick = (e) => {
this.props.onFinished(false);
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
};
private renderSection(kind: "recents"|"suggestions") {
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this);
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
let sectionSubname = null;
if (kind === 'suggestions' && CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
sectionSubname = _t("May include members not in %(communityName)s", { communityName });
}
if (this.props.kind === KIND_INVITE) {
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
@ -1199,7 +1185,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return (
<div className='mx_InviteDialog_section'>
<h3>{ sectionName }</h3>
{ sectionSubname ? <p className="mx_InviteDialog_subname">{ sectionSubname }</p> : null }
{ tiles }
{ showMore }
</div>
@ -1247,7 +1232,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) {
return (
<div className="mx_AddressPickerDialog_identityServer">{ _t(
<div className="mx_InviteDialog_identityServer">{ _t(
"Use an identity server to invite by email. " +
"<default>Use the default (%(defaultIdentityServerName)s)</default> " +
"or manage in <settings>Settings</settings>.",
@ -1268,7 +1253,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
);
} else {
return (
<div className="mx_AddressPickerDialog_identityServer">{ _t(
<div className="mx_InviteDialog_identityServer">{ _t(
"Use an identity server to invite by email. " +
"Manage in <settings>Settings</settings>.",
{}, {
@ -1377,35 +1362,6 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
);
}
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
const inviteText = _t(
"This won't invite them to %(communityName)s. " +
"To invite someone to %(communityName)s, click <a>here</a>",
{ communityName }, {
userId: () => {
return (
<a
href={makeUserPermalink(userId)}
rel="noreferrer noopener"
target="_blank"
>{ userId }</a>
);
},
a: (sub) => {
return (
<AccessibleButton
kind="link"
onClick={this.onCommunityInviteClick}
>{ sub }</AccessibleButton>
);
},
},
);
helpText = <React.Fragment>
{ helpText } { inviteText }
</React.Fragment>;
}
buttonText = _t("Go");
goButtonFn = this.startDm;
extraSection = <div className="mx_InviteDialog_section_hidden_suggestions_disclaimer">
@ -1423,7 +1379,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
</div>;
} else if (this.props.kind === KIND_INVITE) {
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
const isSpace = SpaceStore.spacesEnabled && room?.isSpaceRoom();
const isSpace = room?.isSpaceRoom();
title = isSpace
? _t("Invite to %(spaceName)s", {
spaceName: room.name || _t("Unnamed Space"),

View file

@ -18,13 +18,12 @@ limitations under the License.
import * as React from 'react';
import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";
import { Group } from "matrix-js-sdk/src/models/group";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler';
import QRCode from "../elements/QRCode";
import { RoomPermalinkCreator, makeGroupPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import { RoomPermalinkCreator, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import { selectText } from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
import { IDialogProps } from "./IDialogProps";
@ -63,7 +62,7 @@ const socials = [
];
interface IProps extends IDialogProps {
target: Room | User | Group | RoomMember | MatrixEvent;
target: Room | User | RoomMember | MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
}
@ -121,8 +120,6 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
matrixToUrl = makeUserPermalink(this.props.target.userId);
} else if (this.props.target instanceof Group) {
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
} else if (this.props.target instanceof MatrixEvent) {
if (this.state.linkSpecificEvent) {
matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId());
@ -153,8 +150,6 @@ export default class ShareDialog extends React.PureComponent<IProps, IState> {
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
title = _t('Share User');
} else if (this.props.target instanceof Group) {
title = _t('Share Community');
} else if (this.props.target instanceof MatrixEvent) {
title = _t('Share Room Message');
checkbox = <div>

View file

@ -28,7 +28,6 @@ import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserS
import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import SdkConfig from "../../../SdkConfig";
import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab";
import { UIFeature } from "../../../settings/UIFeature";
@ -41,7 +40,6 @@ import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsT
export enum UserTab {
General = "USER_GENERAL_TAB",
Appearance = "USER_APPEARANCE_TAB",
Flair = "USER_FLAIR_TAB",
Notifications = "USER_NOTIFICATIONS_TAB",
Preferences = "USER_PREFERENCES_TAB",
Keyboard = "USER_KEYBOARD_TAB",
@ -103,15 +101,6 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
<AppearanceUserSettingsTab />,
"UserSettingsAppearance",
));
if (SettingsStore.getValue(UIFeature.Flair)) {
tabs.push(new Tab(
UserTab.Flair,
_td("Flair"),
"mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />,
"UserSettingFlair",
));
}
tabs.push(new Tab(
UserTab.Notifications,
_td("Notifications"),

View file

@ -1,46 +0,0 @@
/* eslint new-cap: "off" */
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import TagTile from './TagTile';
import ContextMenu, { toRightOf, useContextMenu } from "../../structures/ContextMenu";
import * as sdk from '../../../index';
export default function DNDTagTile(props) {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu = null;
if (menuDisplayed && handle.current) {
const elementRect = handle.current.getBoundingClientRect();
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(elementRect)} onFinished={closeMenu}>
<TagTileContextMenu tag={props.tag} onFinished={closeMenu} index={props.index} />
</ContextMenu>
);
}
return <>
<TagTile
{...props}
contextMenuButtonRef={handle}
menuDisplayed={menuDisplayed}
openMenu={openMenu}
/>
{ contextMenu }
</>;
}

View file

@ -98,7 +98,7 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> {
{ _t(
"Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited, which UI elements you " +
"the rooms you have visited, which UI elements you " +
"last interacted with, and the usernames of other users. " +
"They do not contain messages.",
) }</p>

View file

@ -21,9 +21,7 @@ import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/enums/Layout";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from './Spinner';
@ -133,7 +131,6 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
<EventTile
mxEvent={event}
layout={this.props.layout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
as="div"
/>
</div>;

View file

@ -1,137 +0,0 @@
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { logger } from "matrix-js-sdk/src/logger";
import FlairStore from '../../../stores/FlairStore';
import dis from '../../../dispatcher/dispatcher';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
class FlairAvatar extends React.Component {
constructor() {
super();
this.onClick = this.onClick.bind(this);
}
onClick(ev) {
ev.preventDefault();
// Don't trigger onClick of parent element
ev.stopPropagation();
dis.dispatch({
action: 'view_group',
group_id: this.props.groupProfile.groupId,
});
}
render() {
const httpUrl = mediaFromMxc(this.props.groupProfile.avatarUrl).getSquareThumbnailHttp(16);
const tooltip = this.props.groupProfile.name ?
`${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`:
this.props.groupProfile.groupId;
return <img
src={httpUrl}
width="16"
height="16"
onClick={this.onClick}
title={tooltip} />;
}
}
FlairAvatar.propTypes = {
groupProfile: PropTypes.shape({
groupId: PropTypes.string.isRequired,
name: PropTypes.string,
avatarUrl: PropTypes.string.isRequired,
}),
};
FlairAvatar.contextType = MatrixClientContext;
@replaceableComponent("views.elements.Flair")
export default class Flair extends React.Component {
constructor() {
super();
this.state = {
profiles: [],
};
}
componentDidMount() {
this._unmounted = false;
this._generateAvatars(this.props.groups);
}
componentWillUnmount() {
this._unmounted = true;
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
this._generateAvatars(newProps.groups);
}
async _getGroupProfiles(groups) {
const profiles = [];
for (const groupId of groups) {
let groupProfile = null;
try {
groupProfile = await FlairStore.getGroupProfileCached(this.context, groupId);
} catch (err) {
logger.error('Could not get profile for group', groupId, err);
}
profiles.push(groupProfile);
}
return profiles.filter((p) => p !== null);
}
async _generateAvatars(groups) {
if (!groups || groups.length === 0) {
return;
}
const profiles = await this._getGroupProfiles(groups);
if (!this.unmounted) {
this.setState({
profiles: profiles.filter((profile) => {
return profile ? profile.avatarUrl : false;
}),
});
}
}
render() {
if (this.state.profiles.length === 0) {
return null;
}
const avatars = this.state.profiles.map((profile, index) => {
return <FlairAvatar key={index} groupProfile={profile} />;
});
return (
<span className="mx_Flair">
{ avatars }
</span>
);
}
}
Flair.propTypes = {
groups: PropTypes.arrayOf(PropTypes.string),
};
Flair.contextType = MatrixClientContext;

View file

@ -23,11 +23,9 @@ import { logger } from "matrix-js-sdk/src/logger";
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import FlairStore from "../../../stores/FlairStore";
import { getPrimaryPermalinkEntity, parsePermalink } from "../../../utils/permalinks/Permalinks";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Action } from "../../../dispatcher/actions";
import { mediaFromMxc } from "../../../customisations/Media";
import Tooltip from './Tooltip';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -43,7 +41,6 @@ class Pill extends React.Component {
static TYPE_USER_MENTION = 'TYPE_USER_MENTION';
static TYPE_ROOM_MENTION = 'TYPE_ROOM_MENTION';
static TYPE_GROUP_MENTION = 'TYPE_GROUP_MENTION';
static TYPE_AT_ROOM_MENTION = 'TYPE_AT_ROOM_MENTION'; // '@room' mention
static propTypes = {
@ -69,8 +66,6 @@ class Pill extends React.Component {
// The member related to the user pill
member: null,
// The group related to the group pill
group: null,
// The room related to the room pill
room: null,
// Is the user hovering the pill
@ -98,11 +93,9 @@ class Pill extends React.Component {
'@': Pill.TYPE_USER_MENTION,
'#': Pill.TYPE_ROOM_MENTION,
'!': Pill.TYPE_ROOM_MENTION,
'+': Pill.TYPE_GROUP_MENTION,
}[prefix];
let member;
let group;
let room;
switch (pillType) {
case Pill.TYPE_AT_ROOM_MENTION: {
@ -116,8 +109,8 @@ class Pill extends React.Component {
member = new RoomMember(null, resourceId);
this.doProfileLookup(resourceId, member);
}
}
break;
}
case Pill.TYPE_ROOM_MENTION: {
const localRoom = resourceId[0] === '#' ?
MatrixClientPeg.get().getRooms().find((r) => {
@ -130,23 +123,10 @@ class Pill extends React.Component {
// a room avatar and name.
// this.doRoomProfileLookup(resourceId, member);
}
}
break;
case Pill.TYPE_GROUP_MENTION: {
const cli = MatrixClientPeg.get();
try {
group = await FlairStore.getGroupProfileCached(cli, resourceId);
} catch (e) { // if FlairStore failed, fall back to just groupId
group = {
groupId: resourceId,
avatarUrl: null,
name: null,
};
}
}
}
this.setState({ resourceId, pillType, member, group, room });
this.setState({ resourceId, pillType, member, room });
}
componentDidMount() {
@ -203,7 +183,6 @@ class Pill extends React.Component {
};
render() {
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
@ -225,8 +204,8 @@ class Pill extends React.Component {
}
pillClass = 'mx_AtRoomPill';
}
}
break;
}
case Pill.TYPE_USER_MENTION: {
// If this user is not a member of this room, default to the empty member
const member = this.state.member;
@ -241,8 +220,8 @@ class Pill extends React.Component {
href = null;
onClick = this.onUserPillClicked;
}
}
break;
}
case Pill.TYPE_ROOM_MENTION: {
const room = this.state.room;
if (room) {
@ -252,25 +231,8 @@ class Pill extends React.Component {
}
}
pillClass = 'mx_RoomPill';
}
break;
case Pill.TYPE_GROUP_MENTION: {
if (this.state.group) {
const { avatarUrl, groupId, name } = this.state.group;
linkText = groupId;
if (this.props.shouldShowPillAvatar) {
avatar = <BaseAvatar
name={name || groupId}
width={16}
height={16}
aria-hidden="true"
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(16) : null} />;
}
pillClass = 'mx_GroupPill';
}
}
break;
}
const classes = classNames("mx_Pill", pillClass, {

View file

@ -1,193 +0,0 @@
/*
Copyright 2017 New Vector Ltd.
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { logger } from "matrix-js-sdk/src/logger";
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import FlairStore from '../../../stores/FlairStore';
import GroupStore from '../../../stores/GroupStore';
import GroupFilterOrderStore from '../../../stores/GroupFilterOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
import { mediaFromMxc } from "../../../customisations/Media";
import { replaceableComponent } from "../../../utils/replaceableComponent";
// A class for a child of GroupFilterPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
// - Rooms that are part of the group
// - Direct messages with members of the group
// with the intention that this could be expanded to arbitrary tags in future.
@replaceableComponent("views.elements.TagTile")
export default class TagTile extends React.Component {
static propTypes = {
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
// For now, only group IDs are handled.
tag: PropTypes.string,
contextMenuButtonRef: PropTypes.object,
openMenu: PropTypes.func,
menuDisplayed: PropTypes.bool,
selected: PropTypes.bool,
};
static contextType = MatrixClientContext;
state = {
// Whether the mouse is over the tile
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
};
componentDidMount() {
this.unmounted = false;
if (this.props.tag[0] === '+') {
FlairStore.addListener('updateGroupProfile', this._onFlairStoreUpdated);
this._onFlairStoreUpdated();
// New rooms or members may have been added to the group, fetch async
this._refreshGroup(this.props.tag);
}
}
componentWillUnmount() {
this.unmounted = true;
if (this.props.tag[0] === '+') {
FlairStore.removeListener('updateGroupProfile', this._onFlairStoreUpdated);
}
}
_onFlairStoreUpdated = () => {
if (this.unmounted) return;
FlairStore.getGroupProfileCached(
this.context,
this.props.tag,
).then((profile) => {
if (this.unmounted) return;
this.setState({ profile });
}).catch((err) => {
logger.warn('Could not fetch group profile for ' + this.props.tag, err);
});
};
_refreshGroup(groupId) {
GroupStore.refreshGroupRooms(groupId);
GroupStore.refreshGroupMembers(groupId);
}
onClick = e => {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'select_tag',
tag: this.props.tag,
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
shiftKey: e.shiftKey,
});
if (this.props.tag[0] === '+') {
// New rooms or members may have been added to the group, fetch async
this._refreshGroup(this.props.tag);
}
};
onMouseOver = () => {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) return;
this.setState({ hover: true });
};
onMouseLeave = () => {
this.setState({ hover: false });
};
openMenu = e => {
// Prevent the TagTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
if (SettingsStore.getValue("feature_communities_v2_prototypes")) return;
this.setState({ hover: false });
this.props.openMenu();
};
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const profile = this.state.profile || {};
const name = profile.name || this.props.tag;
const avatarSize = 32;
const httpUrl = profile.avatarUrl
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarSize)
: null;
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
const className = classNames({
mx_TagTile: true,
mx_TagTile_prototype: isPrototype,
mx_TagTile_selected: this.props.selected && !isPrototype,
mx_TagTile_selected_prototype: this.props.selected && isPrototype,
});
const badge = GroupFilterOrderStore.getGroupBadge(this.props.tag);
let badgeElement;
if (badge && !this.state.hover && !this.props.menuDisplayed) {
const badgeClasses = classNames({
"mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badge.highlight,
});
badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badge.count) }</div>);
}
const contextButton = this.state.hover || this.props.menuDisplayed ?
<AccessibleButton
className="mx_TagTile_context_button"
onClick={this.openMenu}
inputRef={this.props.contextMenuButtonRef}
>
{ "\u00B7\u00B7\u00B7" }
</AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />;
const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton");
return <AccessibleTooltipButton
className={className}
onClick={this.onClick}
onContextMenu={this.openMenu}
title={name}
>
<div
className="mx_TagTile_avatar"
onMouseOver={this.onMouseOver}
onMouseLeave={this.onMouseLeave}
>
<BaseAvatar
name={name}
idName={this.props.tag}
url={httpUrl}
width={avatarSize}
height={avatarSize}
/>
{ contextButton }
{ badgeElement }
</div>
</AccessibleTooltipButton>;
}
}

View file

@ -1,88 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import * as fbEmitter from "fbemitter";
import classNames from "classnames";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
}
interface IState {
selected: boolean;
}
@replaceableComponent("views.elements.UserTagTile")
export default class UserTagTile extends React.PureComponent<IProps, IState> {
private tagStoreRef: fbEmitter.EventSubscription;
constructor(props: IProps) {
super(props);
this.state = {
selected: GroupFilterOrderStore.getSelectedTags().length === 0,
};
}
public componentDidMount() {
this.tagStoreRef = GroupFilterOrderStore.addListener(this.onTagStoreUpdate);
}
public componentWillUnmount() {
this.tagStoreRef.remove();
}
private onTagStoreUpdate = () => {
const selected = GroupFilterOrderStore.getSelectedTags().length === 0;
this.setState({ selected });
};
private onTileClick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
// Deselect all tags
defaultDispatcher.dispatch({ action: "deselect_tags" });
};
public render() {
// XXX: We reuse TagTile classes for ease of demonstration - we should probably generify
// TagTile instead if we continue to use this component.
const className = classNames({
mx_TagTile: true,
mx_TagTile_prototype: true,
mx_TagTile_selected_prototype: this.state.selected,
mx_TagTile_home: true,
});
return (
<AccessibleTooltipButton
className={className}
onClick={this.onTileClick}
title={_t("Home")}
>
<div className="mx_TagTile_avatar">
<div className="mx_TagTile_homeIcon" />
</div>
</AccessibleTooltipButton>
);
}
}

View file

@ -1,207 +0,0 @@
/*
Copyright 2017, 2018 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import ContextMenu, { ContextMenuButton, toRightOf } from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
// XXX this class copies a lot from RoomTile.js
@replaceableComponent("views.groups.GroupInviteTile")
export default class GroupInviteTile extends React.Component {
static propTypes = {
group: PropTypes.object.isRequired,
};
static contextType = MatrixClientContext;
constructor(props, context) {
super(props, context);
this.state = {
hover: false,
badgeHover: false,
menuDisplayed: false,
selected: this.props.group.groupId === null, // XXX: this needs linking to LoggedInView/GroupView state
};
}
onClick = e => {
dis.dispatch({
action: 'view_group',
group_id: this.props.group.groupId,
});
};
onMouseEnter = () => {
const state = { hover: true };
// Only allow non-guests to access the context menu
if (!this.context.isGuest()) {
state.badgeHover = true;
}
this.setState(state);
};
onMouseLeave = () => {
this.setState({
badgeHover: false,
hover: false,
});
};
_showContextMenu(boundingClientRect) {
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
const state = {
contextMenuPosition: boundingClientRect,
};
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
state.hover = false;
}
this.setState(state);
}
onContextMenuButtonClick = e => {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this._showContextMenu(e.target.getBoundingClientRect());
};
onContextMenu = e => {
// Prevent the native context menu
e.preventDefault();
this._showContextMenu({
right: e.clientX,
top: e.clientY,
height: 0,
});
};
closeMenu = () => {
this.setState({
contextMenuPosition: null,
});
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const groupName = this.props.group.name || this.props.group.groupId;
const httpAvatarUrl = this.props.group.avatarUrl
? mediaFromMxc(this.props.group.avatarUrl).getSquareThumbnailHttp(24)
: null;
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
const nameClasses = classNames('mx_RoomTile_title mx_RoomTile_invite mx_RoomTile_badgeShown', {
'mx_RoomTile_badgeShown': this.state.badgeHover || isMenuDisplayed,
});
// XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
const label = <div title={this.props.group.groupId} className={nameClasses} tabIndex={-1} dir="auto">
{ groupName }
</div>;
const badgeEllipsis = this.state.badgeHover || isMenuDisplayed;
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
'mx_RoomTile_badgeButton': badgeEllipsis,
});
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
let tooltip;
if (this.props.collapsed && this.state.hover) {
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
}
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
'mx_RoomTile_menuDisplayed': isMenuDisplayed,
'mx_RoomTile_selected': this.state.selected,
'mx_GroupInviteTile': true,
});
let contextMenu;
if (isMenuDisplayed) {
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
<GroupInviteTileContextMenu group={this.props.group} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<RovingTabIndexWrapper>
{ ({ onFocus, isActive, ref }) =>
<AccessibleButton
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
inputRef={ref}
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
>
<div className="mx_RoomTile_avatar">
{ av }
</div>
<div className="mx_RoomTile_details">
<div className="mx_RoomTile_primaryDetails">
<div className="mx_RoomTile_titleContainer">
{ label }
<ContextMenuButton
className={badgeClasses}
onClick={this.onContextMenuButtonClick}
label={_t("Options")}
isExpanded={isMenuDisplayed}
tabIndex={isActive ? 0 : -1}
>
{ badgeContent }
</ContextMenuButton>
</div>
</div>
</div>
{ tooltip }
</AccessibleButton>
}
</RovingTabIndexWrapper>
{ contextMenu }
</React.Fragment>;
}
}

View file

@ -1,244 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import GroupStore from '../../../stores/GroupStore';
import { showGroupInviteDialog } from '../../../GroupAddressPicker';
import AccessibleButton from '../elements/AccessibleButton';
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
const INITIAL_LOAD_NUM_MEMBERS = 30;
@replaceableComponent("views.groups.GroupMemberList")
export default class GroupMemberList extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
};
state = {
members: null,
membersError: null,
invitedMembers: null,
invitedMembersError: null,
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
};
componentDidMount() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
}
componentWillUnmount() {
this._unmounted = true;
}
_initGroupStore(groupId) {
GroupStore.registerListener(groupId, () => {
this._fetchMembers();
});
GroupStore.on('error', (err, errorGroupId, stateKey) => {
if (this._unmounted || groupId !== errorGroupId) return;
if (stateKey === GroupStore.STATE_KEY.GroupMembers) {
this.setState({
membersError: err,
});
}
if (stateKey === GroupStore.STATE_KEY.GroupInvitedMembers) {
this.setState({
invitedMembersError: err,
});
}
});
}
_fetchMembers() {
if (this._unmounted) return;
this.setState({
members: GroupStore.getGroupMembers(this.props.groupId),
invitedMembers: GroupStore.getGroupInvitedMembers(this.props.groupId),
});
}
_createOverflowTile = (overflowCount, totalCount) => {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." width={36} height={36} />
}
name={text}
presenceState="online"
suppressOnHover={true}
onClick={this._showFullMemberList}
/>
);
};
_showFullMemberList = () => {
this.setState({
truncateAt: -1,
});
};
onSearchQueryChanged = ev => {
this.setState({ searchQuery: ev.target.value });
};
makeGroupMemberTiles(query, memberList, memberListError) {
if (memberListError) {
return <div className="warning">{ _t("Failed to load group members") }</div>;
}
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
const TruncatedList = sdk.getComponent("elements.TruncatedList");
query = (query || "").toLowerCase();
if (query) {
memberList = memberList.filter((m) => {
const matchesName = (m.displayname || "").toLowerCase().includes(query);
const matchesId = m.userId.toLowerCase().includes(query);
if (!matchesName && !matchesId) {
return false;
}
return true;
});
}
const uniqueMembers = {};
memberList.forEach((m) => {
if (!uniqueMembers[m.userId]) uniqueMembers[m.userId] = m;
});
memberList = Object.keys(uniqueMembers).map((userId) => uniqueMembers[userId]);
// Descending sort on isPrivileged = true = 1 to isPrivileged = false = 0
memberList.sort((a, b) => {
if (a.isPrivileged === b.isPrivileged) {
const aName = a.displayname || a.userId;
const bName = b.displayname || b.userId;
if (aName < bName) {
return -1;
} else if (aName > bName) {
return 1;
} else {
return 0;
}
} else {
return a.isPrivileged ? -1 : 1;
}
});
const memberTiles = memberList.map((m) => {
return (
<GroupMemberTile key={m.userId} groupId={this.props.groupId} member={m} />
);
});
return <TruncatedList
className="mx_MemberList_wrapper"
truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}
>
{ memberTiles }
</TruncatedList>;
}
onInviteToGroupButtonClick = () => {
showGroupInviteDialog(this.props.groupId).then(() => {
RightPanelStore.instance.setCard({
phase: RightPanelPhases.GroupMemberList,
state: { groupId: this.props.groupId },
});
});
};
render() {
if (this.state.fetching || this.state.fetchingInvitedMembers) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_MemberList">
<Spinner />
</div>);
}
const inputBox = (
<input
className="mx_GroupMemberList_query mx_textinput"
id="mx_GroupMemberList_query"
type="text"
onChange={this.onSearchQueryChanged}
value={this.state.searchQuery}
placeholder={_t('Filter community members')}
autoComplete="off"
/>
);
const joined = this.state.members ? <div className="mx_MemberList_joined">
{
this.makeGroupMemberTiles(
this.state.searchQuery,
this.state.members,
this.state.membersError,
)
}
</div> : <div />;
const invited = (this.state.invitedMembers && this.state.invitedMembers.length > 0) ?
<div className="mx_MemberList_invited">
<h2>{ _t("Invited") }</h2>
{
this.makeGroupMemberTiles(
this.state.searchQuery,
this.state.invitedMembers,
this.state.invitedMembersError,
)
}
</div> : <div />;
let inviteButton;
if (GroupStore.isUserPrivileged(this.props.groupId)) {
inviteButton = (
<AccessibleButton
className="mx_MemberList_invite mx_MemberList_inviteCommunity"
onClick={this.onInviteToGroupButtonClick}
>
<span>{ _t('Invite to this community') }</span>
</AccessibleButton>
);
}
return (
<div className="mx_MemberList" role="tabpanel">
{ inviteButton }
<AutoHideScrollbar>
{ joined }
{ invited }
</AutoHideScrollbar>
{ inputBox }
</div>
);
}
}

View file

@ -1,77 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { GroupMemberType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
@replaceableComponent("views.groups.GroupMemberTile")
export default class GroupMemberTile extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
member: GroupMemberType.isRequired,
};
static contextType = MatrixClientContext;
onClick = e => {
dis.dispatch({
action: 'view_group_user',
member: this.props.member,
groupId: this.props.groupId,
});
};
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const EntityTile = sdk.getComponent('rooms.EntityTile');
const name = this.props.member.displayname || this.props.member.userId;
const avatarUrl = this.props.member.avatarUrl
? mediaFromMxc(this.props.member.avatarUrl).getSquareThumbnailHttp(36)
: null;
const av = (
<BaseAvatar
aria-hidden="true"
name={this.props.member.displayname || this.props.member.userId}
idName={this.props.member.userId}
width={36}
height={36}
url={avatarUrl}
/>
);
return (
<EntityTile
name={name}
avatarJsx={av}
onClick={this.onClick}
suppressOnHover={true}
presenceState="online"
powerStatus={this.props.member.isPrivileged ? EntityTile.POWER_STATUS_ADMIN : null}
/>
);
}
}

View file

@ -1,76 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import GroupStore from '../../../stores/GroupStore';
import ToggleSwitch from "../elements/ToggleSwitch";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.groups.GroupPublicityTile")
export default class GroupPublicityToggle extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
};
state = {
busy: false,
ready: false,
isGroupPublicised: false, // assume false as <ToggleSwitch /> expects a boolean
};
componentDidMount() {
this._initGroupStore(this.props.groupId);
}
_initGroupStore(groupId) {
this._groupStoreToken = GroupStore.registerListener(groupId, () => {
this.setState({
isGroupPublicised: Boolean(GroupStore.getGroupPublicity(groupId)),
ready: GroupStore.isStateReady(groupId, GroupStore.STATE_KEY.Summary),
});
});
}
componentWillUnmount() {
if (this._groupStoreToken) this._groupStoreToken.unregister();
}
_onPublicityToggle = () => {
this.setState({
busy: true,
// Optimistic early update
isGroupPublicised: !this.state.isGroupPublicised,
});
GroupStore.setGroupPublicity(this.props.groupId, !this.state.isGroupPublicised).then(() => {
this.setState({
busy: false,
});
});
};
render() {
const GroupTile = sdk.getComponent('groups.GroupTile');
return <div className="mx_GroupPublicity_toggle">
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} />
<ToggleSwitch checked={this.state.isGroupPublicised}
disabled={!this.state.ready || this.state.busy}
onChange={this._onPublicityToggle} />
</div>;
}
}

View file

@ -1,236 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { logger } from "matrix-js-sdk/src/logger";
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import GroupStore from '../../../stores/GroupStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
@replaceableComponent("views.groups.GroupRoomInfo")
export default class GroupRoomInfo extends React.Component {
static contextType = MatrixClientContext;
static propTypes = {
groupId: PropTypes.string,
groupRoomId: PropTypes.string,
};
state = {
isUserPrivilegedInGroup: null,
groupRoom: null,
groupRoomPublicityLoading: false,
groupRoomRemoveLoading: false,
};
componentDidMount() {
this._initGroupStore(this.props.groupId);
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.groupId !== this.props.groupId) {
this._unregisterGroupStore(this.props.groupId);
this._initGroupStore(newProps.groupId);
}
}
componentWillUnmount() {
this._unregisterGroupStore(this.props.groupId);
}
_initGroupStore(groupId) {
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
}
_unregisterGroupStore(groupId) {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
}
_updateGroupRoom() {
this.setState({
groupRoom: GroupStore.getGroupRooms(this.props.groupId).find(
(r) => r.roomId === this.props.groupRoomId,
),
});
}
onGroupStoreUpdated = () => {
this.setState({
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
this._updateGroupRoom();
};
_onRemove = e => {
const groupId = this.props.groupId;
const roomName = this.state.groupRoom.displayname;
e.preventDefault();
e.stopPropagation();
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Confirm removal of group from room', '', QuestionDialog, {
title: _t("Are you sure you want to remove '%(roomName)s' from %(groupId)s?", { roomName, groupId }),
description: _t("Removing a room from the community will also remove it from the community page."),
button: _t("Remove"),
onFinished: (proceed) => {
if (!proceed) return;
this.setState({ groupRoomRemoveLoading: true });
const groupId = this.props.groupId;
const roomId = this.props.groupRoomId;
GroupStore.removeRoomFromGroup(this.props.groupId, roomId).then(() => {
dis.dispatch({
action: "view_group_room_list",
});
}).catch((err) => {
logger.error(`Error whilst removing ${roomId} from ${groupId}`, err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
title: _t("Failed to remove room from community"),
description: _t(
"Failed to remove '%(roomName)s' from %(groupId)s", { groupId, roomName },
),
});
}).finally(() => {
this.setState({ groupRoomRemoveLoading: false });
});
},
});
};
_onCancel = e => {
dis.dispatch({
action: "view_group_room_list",
});
};
_changeGroupRoomPublicity = e => {
const isPublic = e.target.value === "public";
this.setState({
groupRoomPublicityLoading: true,
});
const groupId = this.props.groupId;
const roomId = this.props.groupRoomId;
const roomName = this.state.groupRoom.displayname;
GroupStore.updateGroupRoomVisibility(this.props.groupId, roomId, isPublic).catch((err) => {
logger.error(`Error whilst changing visibility of ${roomId} in ${groupId} to ${isPublic}`, err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove room from group', '', ErrorDialog, {
title: _t("Something went wrong!"),
description: _t(
"The visibility of '%(roomName)s' in %(groupId)s could not be updated.",
{ roomName, groupId },
),
});
}).finally(() => {
this.setState({
groupRoomPublicityLoading: false,
});
});
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) {
const Spinner = sdk.getComponent("elements.Spinner");
return <div className="mx_MemberInfo">
<Spinner />
</div>;
}
let adminTools;
if (this.state.isUserPrivilegedInGroup) {
adminTools =
<div className="mx_MemberInfo_adminTools">
<h3>{ _t("Admin Tools") }</h3>
<div className="mx_MemberInfo_buttons">
<AccessibleButton className="mx_MemberInfo_field" onClick={this._onRemove}>
{ _t('Remove from community') }
</AccessibleButton>
</div>
<h3>
{ _t('Visibility in Room List') }
{ this.state.groupRoomPublicityLoading ?
<InlineSpinner /> : <div />
}
</h3>
<div>
<label>
<input type="radio"
value="public"
checked={this.state.groupRoom.isPublic}
onChange={this._changeGroupRoomPublicity}
/>
<div className="mx_MemberInfo_label_text">
{ _t('Visible to everyone') }
</div>
</label>
</div>
<div>
<label>
<input type="radio"
value="private"
checked={!this.state.groupRoom.isPublic}
onChange={this._changeGroupRoomPublicity}
/>
<div className="mx_MemberInfo_label_text">
{ _t('Only visible to community members') }
</div>
</label>
</div>
</div>;
}
const avatarUrl = this.state.groupRoom.avatarUrl;
let avatarElement;
if (avatarUrl) {
const httpUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(800);
avatarElement = <div className="mx_MemberInfo_avatar"><img src={httpUrl} /></div>;
}
const groupRoomName = this.state.groupRoom.displayname;
return (
<div className="mx_MemberInfo" role="tabpanel">
<AutoHideScrollbar>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
<img src={require("../../../../res/img/cancel.svg").default} width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton>
{ avatarElement }
<h2>{ groupRoomName }</h2>
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{ this.state.groupRoom.canonicalAlias }
</div>
</div>
{ adminTools }
</AutoHideScrollbar>
</div>
);
}
}

View file

@ -1,179 +0,0 @@
/*
Copyright 2017 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import GroupStore from '../../../stores/GroupStore';
import { showGroupAddRoomDialog } from '../../../GroupAddressPicker';
import AccessibleButton from '../elements/AccessibleButton';
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const INITIAL_LOAD_NUM_ROOMS = 30;
@replaceableComponent("views.groups.GroupRoomList")
export default class GroupRoomList extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
};
state = {
rooms: null,
truncateAt: INITIAL_LOAD_NUM_ROOMS,
searchQuery: "",
};
componentDidMount() {
this._unmounted = false;
this._initGroupStore(this.props.groupId);
}
componentWillUnmount() {
this._unmounted = true;
this._unregisterGroupStore();
}
_unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
}
_initGroupStore(groupId) {
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
// XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
// XXX: This is also leaked - we should remove it when unmounting
GroupStore.on('error', (err, errorGroupId) => {
if (errorGroupId !== groupId) return;
this.setState({
rooms: null,
});
});
}
onGroupStoreUpdated = () => {
if (this._unmounted) return;
this.setState({
rooms: GroupStore.getGroupRooms(this.props.groupId),
});
};
_createOverflowTile = (overflowCount, totalCount) => {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." width={36} height={36} />
}
name={text}
presenceState="online"
suppressOnHover={true}
onClick={this._showFullRoomList}
/>
);
};
_showFullRoomList = () => {
this.setState({
truncateAt: -1,
});
};
onSearchQueryChanged = ev => {
this.setState({ searchQuery: ev.target.value });
};
onAddRoomToGroupButtonClick = () => {
showGroupAddRoomDialog(this.props.groupId).then(() => {
this.forceUpdate();
});
};
makeGroupRoomTiles(query) {
const GroupRoomTile = sdk.getComponent("groups.GroupRoomTile");
query = (query || "").toLowerCase();
let roomList = this.state.rooms;
if (query) {
roomList = roomList.filter((room) => {
const matchesName = (room.name || "").toLowerCase().includes(query);
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
return matchesName || matchesAlias;
});
}
roomList = roomList.map((groupRoom, index) => {
return (
<GroupRoomTile
key={index}
groupId={this.props.groupId}
groupRoom={groupRoom} />
);
});
return roomList;
}
render() {
if (this.state.rooms === null) {
return null;
}
let inviteButton;
if (GroupStore.isUserPrivileged(this.props.groupId)) {
inviteButton = (
<AccessibleButton
className="mx_MemberList_invite mx_MemberList_addRoomToCommunity"
onClick={this.onAddRoomToGroupButtonClick}
>
<span>{ _t('Add rooms to this community') }</span>
</AccessibleButton>
);
}
const inputBox = (
<input
className="mx_GroupRoomList_query mx_textinput"
id="mx_GroupRoomList_query"
type="text"
onChange={this.onSearchQueryChanged}
value={this.state.searchQuery}
placeholder={_t('Filter community rooms')}
autoComplete="off"
/>
);
const TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_GroupRoomList" role="tabpanel">
{ inviteButton }
<AutoHideScrollbar className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<TruncatedList
className="mx_GroupRoomList_wrapper"
truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}
>
{ this.makeGroupRoomTiles(this.state.searchQuery) }
</TruncatedList>
</AutoHideScrollbar>
{ inputBox }
</div>
);
}
}

View file

@ -1,73 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { GroupRoomType } from '../../../groups';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
@replaceableComponent("views.groups.GroupRoomTile")
class GroupRoomTile extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
groupRoom: GroupRoomType.isRequired,
};
static contextType = MatrixClientContext;
onClick = e => {
dis.dispatch({
action: 'view_group_room',
groupId: this.props.groupId,
groupRoomId: this.props.groupRoom.roomId,
});
};
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const avatarUrl = this.props.groupRoom.avatarUrl
? mediaFromMxc(this.props.groupRoom.avatarUrl).getSquareThumbnailHttp(36)
: null;
const av = (
<BaseAvatar
name={this.props.groupRoom.displayname}
width={36}
height={36}
url={avatarUrl}
/>
);
return (
<AccessibleButton className="mx_GroupRoomTile" onClick={this.onClick}>
<div className="mx_GroupRoomTile_avatar">
{ av }
</div>
<div className="mx_GroupRoomTile_name">
{ this.props.groupRoom.displayname }
</div>
</AccessibleButton>
);
}
}
export default GroupRoomTile;

View file

@ -1,123 +0,0 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { logger } from "matrix-js-sdk/src/logger";
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { _t } from "../../../languageHandler";
import TagOrderActions from "../../../actions/TagOrderActions";
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
@replaceableComponent("views.groups.GroupTile")
class GroupTile extends React.Component {
static propTypes = {
groupId: PropTypes.string.isRequired,
// Whether to show the short description of the group on the tile
showDescription: PropTypes.bool,
// Height of the group avatar in pixels
avatarHeight: PropTypes.number,
};
static contextType = MatrixClientContext;
static defaultProps = {
showDescription: true,
avatarHeight: 50,
};
state = {
profile: null,
};
componentDidMount() {
FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => {
this.setState({ profile });
}).catch((err) => {
logger.error('Error whilst getting cached profile for GroupTile', err);
});
}
onClick = e => {
e.preventDefault();
dis.dispatch({
action: 'view_group',
group_id: this.props.groupId,
});
};
onPinClick = e => {
e.preventDefault();
e.stopPropagation();
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.groupId, 0));
};
onUnpinClick = e => {
e.preventDefault();
e.stopPropagation();
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.groupId));
};
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const profile = this.state.profile || {};
const name = profile.name || this.props.groupId;
const avatarHeight = this.props.avatarHeight;
const descElement = this.props.showDescription ?
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
<div />;
const httpUrl = profile.avatarUrl
? mediaFromMxc(profile.avatarUrl).getSquareThumbnailHttp(avatarHeight)
: null;
const avatarElement = (
<div className="mx_GroupTile_avatar">
<BaseAvatar
name={name}
idName={this.props.groupId}
url={httpUrl}
width={avatarHeight}
height={avatarHeight} />
</div>
);
return <AccessibleButton className="mx_GroupTile" onClick={this.onClick}>
{ avatarElement }
<div className="mx_GroupTile_profile">
<div className="mx_GroupTile_name">{ name }</div>
{ descElement }
<div className="mx_GroupTile_groupId">{ this.props.groupId }</div>
{ !(GroupFilterOrderStore.getOrderedTags() || []).includes(this.props.groupId)
? <AccessibleButton kind="link" onClick={this.onPinClick}>
{ _t("Pin") }
</AccessibleButton>
: <AccessibleButton kind="link" onClick={this.onUnpinClick}>
{ _t("Unpin") }
</AccessibleButton>
}
</div>
</AccessibleButton>;
}
}
export default GroupTile;

View file

@ -1,71 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { logger } from "matrix-js-sdk/src/logger";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.groups.GroupUserSettings")
export default class GroupUserSettings extends React.Component {
static contextType = MatrixClientContext;
state = {
error: null,
groups: null,
};
componentDidMount() {
this.context.getJoinedGroups().then((result) => {
this.setState({ groups: result.groups || [], error: null });
}, (err) => {
logger.error(err);
this.setState({ groups: null, error: err });
});
}
render() {
let text = "";
let groupPublicityToggles = null;
const groups = this.state.groups;
if (this.state.error) {
text = _t('Something went wrong when trying to get your communities.');
} else if (groups === null) {
text = _t('Loading...');
} else if (groups.length > 0) {
const GroupPublicityToggle = sdk.getComponent('groups.GroupPublicityToggle');
groupPublicityToggles = groups.map((groupId, index) => {
return <GroupPublicityToggle key={index} groupId={groupId} />;
});
text = _t('Display your community flair in rooms configured to show it.');
} else {
text = _t("You're not currently a member of any communities.");
}
return (
<div>
<p className="mx_SettingsTab_subsectionText">{ text }</p>
<div className='mx_SettingsTab_subsectionText'>
{ groupPublicityToggles }
</div>
</div>
);
}
}

View file

@ -25,7 +25,6 @@ import UserIdentifier from "../../../customisations/UserIdentifier";
interface IProps {
member?: RoomMember;
fallbackName: string;
flair?: JSX.Element;
onClick?(): void;
colored?: boolean;
emphasizeDisplayName?: boolean;
@ -33,7 +32,7 @@ interface IProps {
export default class DisambiguatedProfile extends React.Component<IProps> {
render() {
const { fallbackName, member, flair, colored, emphasizeDisplayName, onClick } = this.props;
const { fallbackName, member, colored, emphasizeDisplayName, onClick } = this.props;
const rawDisplayName = member?.rawDisplayName || fallbackName;
const mxid = member?.userId;
@ -64,7 +63,6 @@ export default class DisambiguatedProfile extends React.Component<IProps> {
{ rawDisplayName }
</span>
{ mxidElement }
{ flair }
</div>
);
}

View file

@ -52,7 +52,6 @@ export default class MImageReplyBody extends MImageBody {
const fileBody = this.getFileBody();
const sender = <SenderProfile
mxEvent={this.props.mxEvent}
enableFlair={false}
/>;
return <div className="mx_MImageReplyBody">

View file

@ -17,10 +17,7 @@
import React from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { MsgType } from "matrix-js-sdk/src/@types/event";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import Flair from '../elements/Flair';
import FlairStore from '../../../stores/FlairStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import DisambiguatedProfile from "./DisambiguatedProfile";
@ -31,78 +28,12 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
interface IProps {
mxEvent: MatrixEvent;
onClick?(): void;
enableFlair: boolean;
}
interface IState {
userGroups: string[];
relatedGroups: string[];
}
@replaceableComponent("views.messages.SenderProfile")
export default class SenderProfile extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
export default class SenderProfile extends React.PureComponent<IProps> {
public static contextType = MatrixClientContext;
public context!: React.ContextType<typeof MatrixClientContext>;
private unmounted = false;
constructor(props: IProps) {
super(props);
const senderId = this.props.mxEvent.getSender();
this.state = {
userGroups: FlairStore.cachedPublicisedGroups(senderId) || [],
relatedGroups: [],
};
}
componentDidMount() {
this.updateRelatedGroups();
if (this.state.userGroups.length === 0) {
this.getPublicisedGroups();
}
this.context.on(RoomStateEvent.Events, this.onRoomStateEvents);
}
componentWillUnmount() {
this.unmounted = true;
this.context.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
}
private async getPublicisedGroups() {
const userGroups = await FlairStore.getPublicisedGroupsCached(this.context, this.props.mxEvent.getSender());
if (this.unmounted) return;
this.setState({ userGroups });
}
private onRoomStateEvents = (event: MatrixEvent) => {
if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId()) {
this.updateRelatedGroups();
}
};
private updateRelatedGroups() {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
if (!room) return;
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
this.setState({
relatedGroups: relatedGroupsEvent?.getContent().groups || [],
});
}
private getDisplayedGroups(userGroups?: string[], relatedGroups?: string[]) {
let displayedGroups = userGroups || [];
if (relatedGroups && relatedGroups.length > 0) {
displayedGroups = relatedGroups.filter((groupId) => {
return displayedGroups.includes(groupId);
});
} else {
displayedGroups = [];
}
return displayedGroups;
}
render() {
const { mxEvent, onClick } = this.props;
@ -124,19 +55,9 @@ export default class SenderProfile extends React.Component<IProps, IState> {
return null; // emote message must include the name so don't duplicate it
}
let flair;
if (this.props.enableFlair) {
const displayedGroups = this.getDisplayedGroups(
this.state.userGroups, this.state.relatedGroups,
);
flair = <Flair key='flair' userId={mxEvent.getSender()} groups={displayedGroups} />;
}
return (
<DisambiguatedProfile
fallbackName={mxEvent.getSender() || ""}
flair={flair}
onClick={onClick}
member={member}
colored={true}

View file

@ -1,110 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, { HeaderKind } from './HeaderButtons';
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
import { Action } from "../../../dispatcher/actions";
import { ActionPayload } from "../../../dispatcher/payloads";
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
const GROUP_PHASES = [
RightPanelPhases.GroupMemberInfo,
RightPanelPhases.GroupMemberList,
];
const ROOM_PHASES = [
RightPanelPhases.GroupRoomList,
RightPanelPhases.GroupRoomInfo,
];
interface IProps {}
@replaceableComponent("views.right_panel.GroupHeaderButtons")
export default class GroupHeaderButtons extends HeaderButtons {
constructor(props: IProps) {
super(props, HeaderKind.Group);
}
protected onAction(payload: ActionPayload) {
if (payload.action === Action.ViewUser) {
if ((payload as ViewUserPayload).member) {
RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.GroupRoomInfo },
{ phase: RightPanelPhases.GroupMemberList },
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } },
]);
} else {
this.setPhase(RightPanelPhases.GroupMemberList);
}
} else if (payload.action === "view_group") {
this.setPhase(RightPanelPhases.GroupMemberList);
} else if (payload.action === "view_group_room") {
this.setPhase(
RightPanelPhases.GroupRoomInfo,
{ groupRoomId: payload.groupRoomId, groupId: payload.groupId },
);
} else if (payload.action === "view_group_room_list") {
this.setPhase(RightPanelPhases.GroupRoomList);
} else if (payload.action === "view_group_member_list") {
this.setPhase(RightPanelPhases.GroupMemberList);
} else if (payload.action === "view_group_user") {
this.setPhase(RightPanelPhases.GroupMemberInfo, { member: payload.member });
}
}
private onMembersClicked = () => {
if (this.state.phase === RightPanelPhases.GroupMemberInfo) {
// send the active phase to trigger a toggle
this.setPhase(RightPanelPhases.GroupMemberInfo);
} else {
// This toggles for us, if needed
this.setPhase(RightPanelPhases.GroupMemberList);
}
};
private onRoomsClicked = () => {
// This toggles for us, if needed
this.setPhase(RightPanelPhases.GroupRoomList);
};
renderButtons() {
return <>
<HeaderButton
name="groupMembersButton"
title={_t('Members')}
isHighlighted={this.isPhase(GROUP_PHASES)}
onClick={this.onMembersClicked}
analytics={['Right Panel', 'Group Member List Button', 'click']}
/>
<HeaderButton
name="roomsButton"
title={_t('Rooms')}
isHighlighted={this.isPhase(ROOM_PHASES)}
onClick={this.onRoomsClicked}
analytics={['Right Panel', 'Group Room List Button', 'click']}
/>
</>;
}
}

View file

@ -30,7 +30,6 @@ import { NotificationColor } from '../../../stores/notifications/NotificationCol
export enum HeaderKind {
Room = "room",
Group = "group",
}
interface IState {
@ -91,9 +90,7 @@ export default abstract class HeaderButtons<P = {}> extends React.Component<IPro
private onRightPanelStoreUpdate = () => {
if (this.unmounted) return;
let phase = RightPanelStore.instance.currentCard.phase;
if (!RightPanelStore.instance.isOpenForGroup) {phase = null;}
this.setState({ phase });
this.setState({ phase: RightPanelStore.instance.currentCard.phase });
};
// XXX: Make renderButtons a prop

View file

@ -39,7 +39,6 @@ import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
import RoomViewStore from "../../../stores/RoomViewStore";
import MultiInviter from "../../../utils/MultiInviter";
import GroupStore from "../../../stores/GroupStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
@ -69,7 +68,6 @@ import RoomName from "../elements/RoomName";
import { mediaFromMxc } from "../../../customisations/Media";
import UIStore from "../../../stores/UIStore";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import ConfirmSpaceUserActionDialog from "../dialogs/ConfirmSpaceUserActionDialog";
import { bulkSpaceBehaviour } from "../../../utils/space";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
@ -753,7 +751,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({ member, room, powerLevels,
// if muting self, warn as it may be irreversible
if (target === cli.getUserId()) {
try {
if (!(await warnSelfDemote(SpaceStore.spacesEnabled && room?.isSpaceRoom()))) return;
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
} catch (e) {
logger.error("Failed to warn about self demotion: ", e);
return;
@ -847,7 +845,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
stopUpdating={stopUpdating}
/>;
}
if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
redactButton = (
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
);
@ -885,99 +883,6 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
return <div />;
};
export interface GroupMember {
userId: string;
displayname?: string; // XXX: GroupMember objects are inconsistent :((
avatarUrl?: string;
}
const GroupAdminToolsSection: React.FC<{
groupId: string;
groupMember: GroupMember;
startUpdating(): void;
stopUpdating(): void;
}> = ({ children, groupId, groupMember, startUpdating, stopUpdating }) => {
const cli = useContext(MatrixClientContext);
const [isPrivileged, setIsPrivileged] = useState(false);
const [isInvited, setIsInvited] = useState(false);
// Listen to group store changes
useEffect(() => {
let unmounted = false;
const onGroupStoreUpdated = () => {
if (unmounted) return;
setIsPrivileged(GroupStore.isUserPrivileged(groupId));
setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some(
(m) => m.userId === groupMember.userId,
));
};
GroupStore.registerListener(groupId, onGroupStoreUpdated);
onGroupStoreUpdated();
// Handle unmount
return () => {
unmounted = true;
GroupStore.unregisterListener(onGroupStoreUpdated);
};
}, [groupId, groupMember.userId]);
if (isPrivileged) {
const onKick = async () => {
const { finished } = Modal.createDialog(ConfirmUserActionDialog, {
matrixClient: cli,
groupMember,
action: isInvited ? _t('Disinvite') : _t('Remove from community'),
title: isInvited ? _t('Disinvite this user from community?')
: _t('Remove this user from community?'),
danger: true,
});
const [proceed] = await finished;
if (!proceed) return;
startUpdating();
cli.removeUserFromGroup(groupId, groupMember.userId).then(() => {
// return to the user list
dis.dispatch({
action: Action.ViewUser,
member: null,
});
}).catch((e) => {
Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, {
title: _t('Error'),
description: isInvited ?
_t('Failed to withdraw invitation') :
_t('Failed to remove user from community'),
});
logger.log(e);
}).finally(() => {
stopUpdating();
});
};
const kickButton = (
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_destructive" onClick={onKick}>
{ isInvited ? _t('Disinvite') : _t('Remove from community') }
</AccessibleButton>
);
// No make/revoke admin API yet
/*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator");
giveModButton = <AccessibleButton className="mx_UserInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</AccessibleButton>;*/
return <GenericAdminToolsContainer>
{ kickButton }
{ children }
</GenericAdminToolsContainer>;
}
return <div />;
};
const useIsSynapseAdmin = (cli: MatrixClient) => {
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
@ -1135,7 +1040,7 @@ const PowerLevelEditor: React.FC<{
} else if (myUserId === target) {
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
try {
if (!(await warnSelfDemote(SpaceStore.spacesEnabled && room?.isSpaceRoom()))) return;
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
} catch (e) {
logger.error("Failed to warn about self demotion: ", e);
}
@ -1233,10 +1138,9 @@ export const useDevices = (userId: string) => {
const BasicUserInfo: React.FC<{
room: Room;
member: User | RoomMember;
groupId: string;
devices: IDevice[];
isRoomEncrypted: boolean;
}> = ({ room, member, groupId, devices, isRoomEncrypted }) => {
}> = ({ room, member, devices, isRoomEncrypted }) => {
const cli = useContext(MatrixClientContext);
const powerLevels = useRoomPowerLevels(cli, room);
@ -1338,16 +1242,6 @@ const BasicUserInfo: React.FC<{
{ synapseDeactivateButton }
</RoomAdminToolsContainer>
);
} else if (groupId) {
adminToolsContainer = (
<GroupAdminToolsSection
groupId={groupId}
groupMember={member}
startUpdating={startUpdating}
stopUpdating={stopUpdating}>
{ synapseDeactivateButton }
</GroupAdminToolsSection>
);
} else if (synapseDeactivateButton) {
adminToolsContainer = (
<GenericAdminToolsContainer>
@ -1367,10 +1261,10 @@ const BasicUserInfo: React.FC<{
if (!isRoomEncrypted) {
if (!cryptoEnabled) {
text = _t("This client does not support end-to-end encryption.");
} else if (room && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) {
} else if (room && !room.isSpaceRoom()) {
text = _t("Messages in this room are not end-to-end encrypted.");
}
} else if (!SpaceStore.spacesEnabled || !room.isSpaceRoom()) {
} else if (!room.isSpaceRoom()) {
text = _t("Messages in this room are end-to-end encrypted.");
}
@ -1452,7 +1346,7 @@ const BasicUserInfo: React.FC<{
canInvite={roomPermissions.canInvite}
isIgnored={isIgnored}
member={member as RoomMember}
isSpace={SpaceStore.spacesEnabled && room?.isSpaceRoom()}
isSpace={room?.isSpaceRoom()}
/>
{ adminToolsContainer }
@ -1461,7 +1355,7 @@ const BasicUserInfo: React.FC<{
</React.Fragment>;
};
export type Member = User | RoomMember | GroupMember;
export type Member = User | RoomMember;
const UserInfoHeader: React.FC<{
member: Member;
@ -1540,7 +1434,7 @@ const UserInfoHeader: React.FC<{
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
}
const displayName = (member as RoomMember).rawDisplayName || (member as GroupMember).displayname;
const displayName = (member as RoomMember).rawDisplayName;
return <React.Fragment>
{ avatarElement }
@ -1566,10 +1460,8 @@ const UserInfoHeader: React.FC<{
interface IProps {
user: Member;
groupId?: string;
room?: Room;
phase: RightPanelPhases.RoomMemberInfo
| RightPanelPhases.GroupMemberInfo
| RightPanelPhases.SpaceMemberInfo
| RightPanelPhases.EncryptionPanel;
onClose(): void;
@ -1579,7 +1471,6 @@ interface IProps {
const UserInfo: React.FC<IProps> = ({
user,
groupId,
room,
onClose,
phase = RightPanelPhases.RoomMemberInfo,
@ -1601,10 +1492,10 @@ const UserInfo: React.FC<IProps> = ({
const classes = ["mx_UserInfo"];
let cardState: IRightPanelCardState;
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
// We have no previousPhase for when viewing a UserInfo without a Room at this time
if (room && phase === RightPanelPhases.EncryptionPanel) {
cardState = { member };
} else if (room?.isSpaceRoom() && SpaceStore.spacesEnabled) {
} else if (room?.isSpaceRoom()) {
cardState = { spaceId: room.roomId };
}
@ -1615,13 +1506,11 @@ const UserInfo: React.FC<IProps> = ({
let content;
switch (phase) {
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.GroupMemberInfo:
case RightPanelPhases.SpaceMemberInfo:
content = (
<BasicUserInfo
room={room}
member={member as User}
groupId={groupId as string}
devices={devices}
isRoomEncrypted={isRoomEncrypted}
/>
@ -1649,7 +1538,7 @@ const UserInfo: React.FC<IProps> = ({
}
let scopeHeader;
if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) {
if (room?.isSpaceRoom()) {
scopeHeader = <div data-test-id='space-header' className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />
<RoomName room={room} />

View file

@ -1,126 +0,0 @@
/*
Copyright 2017, 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { logger } from "matrix-js-sdk/src/logger";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import ErrorDialog from "../dialogs/ErrorDialog";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
const GROUP_ID_REGEX = /\+\S+:\S+/;
@replaceableComponent("views.room_settings.RelatedGroupSettings")
export default class RelatedGroupSettings extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
canSetRelatedGroups: PropTypes.bool.isRequired,
relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent),
};
static contextType = MatrixClientContext;
static defaultProps = {
canSetRelatedGroups: false,
};
constructor(props) {
super(props);
this.state = {
newGroupId: "",
newGroupsList: props.relatedGroupsEvent ? (props.relatedGroupsEvent.getContent().groups || []) : [],
};
}
updateGroups(newGroupsList) {
this.context.sendStateEvent(this.props.roomId, 'm.room.related_groups', {
groups: newGroupsList,
}, '').catch((err) => {
logger.error(err);
Modal.createTrackedDialog('Error updating flair', '', ErrorDialog, {
title: _t("Error updating flair"),
description: _t(
"There was an error updating the flair for this room. The server may not allow it or " +
"a temporary error occurred.",
),
});
});
}
validateGroupId(groupId) {
if (!GROUP_ID_REGEX.test(groupId)) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Invalid related community ID', '', ErrorDialog, {
title: _t('Invalid community ID'),
description: _t('\'%(groupId)s\' is not a valid community ID', { groupId }),
});
return false;
}
return true;
}
onNewGroupChanged = (newGroupId) => {
this.setState({ newGroupId });
};
onGroupAdded = (groupId) => {
if (groupId.length === 0 || !this.validateGroupId(groupId)) {
return;
}
const newGroupsList = [...this.state.newGroupsList, groupId];
this.setState({
newGroupsList: newGroupsList,
newGroupId: '',
});
this.updateGroups(newGroupsList);
};
onGroupDeleted = (index) => {
const group = this.state.newGroupsList[index];
const newGroupsList = this.state.newGroupsList.filter((g) => g !== group);
this.setState({ newGroupsList });
this.updateGroups(newGroupsList);
};
render() {
const localDomain = this.context.getDomain();
const EditableItemList = sdk.getComponent('elements.EditableItemList');
return <div>
<EditableItemList
id="relatedGroups"
items={this.state.newGroupsList}
className="mx_RelatedGroupSettings"
newItem={this.state.newGroupId}
canRemove={this.props.canSetRelatedGroups}
canEdit={this.props.canSetRelatedGroups}
onNewItemChanged={this.onNewGroupChanged}
onItemAdded={this.onGroupAdded}
onItemRemoved={this.onGroupDeleted}
itemsLabel={_t('Showing flair for these communities:')}
noItemsLabel={_t('This room is not showing flair for any communities')}
placeholder={_t(
'New community ID (e.g. +foo:%(localDomain)s)', { localDomain },
)}
/>
</div>;
}
}

View file

@ -115,7 +115,6 @@ const stateEventTileTypes = {
[EventType.RoomTombstone]: 'messages.TextualEvent',
[EventType.RoomJoinRules]: 'messages.TextualEvent',
[EventType.RoomGuestAccess]: 'messages.TextualEvent',
'm.room.related_groups': 'messages.TextualEvent', // legacy communities flair
};
const stateEventSingular = new Set([
@ -133,7 +132,6 @@ const stateEventSingular = new Set([
EventType.RoomTombstone,
EventType.RoomJoinRules,
EventType.RoomGuestAccess,
'm.room.related_groups',
]);
// Add all the Mjolnir stuff to the renderer
@ -292,9 +290,6 @@ interface IProps {
// which layout to use
layout?: Layout;
// whether or not to show flair at all
enableFlair?: boolean;
// whether or not to show read receipts
showReadReceipts?: boolean;
@ -1214,12 +1209,10 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
sender = <SenderProfile
onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent}
enableFlair={this.props.enableFlair}
/>;
} else {
sender = <SenderProfile
mxEvent={this.props.mxEvent}
enableFlair={this.props.enableFlair}
/>;
}
}

View file

@ -33,7 +33,6 @@ import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher/dispatcher';
import { isValid3pidInvite } from "../../../RoomInvite";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import BaseCard from "../right_panel/BaseCard";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
@ -46,7 +45,6 @@ import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import EntityTile from "./EntityTile";
import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar';
import SpaceStore from "../../../stores/spaces/SpaceStore";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import PosthogTrackers from "../../../PosthogTrackers";
@ -523,10 +521,7 @@ export default class MemberList extends React.Component<IProps, IState> {
if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) {
let inviteButtonText = _t("Invite to this room");
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat && chat.roomId === this.props.roomId) {
inviteButtonText = _t("Invite to this community");
} else if (SpaceStore.spacesEnabled && room.isSpaceRoom()) {
if (room.isSpaceRoom()) {
inviteButtonText = _t("Invite to this space");
}
@ -566,7 +561,7 @@ export default class MemberList extends React.Component<IProps, IState> {
);
let scopeHeader;
if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) {
if (room?.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />
<RoomName room={room} />
@ -603,7 +598,7 @@ export default class MemberList extends React.Component<IProps, IState> {
return;
}
// call AddressPickerDialog
// open the room inviter
dis.dispatch({
action: 'view_invite',
roomId: this.props.roomId,

View file

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

View file

@ -25,18 +25,15 @@ import ResizeNotifier from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore";
import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, isCustomTag, TagID } from "../../../stores/room-list/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
import { ActionPayload } from "../../../dispatcher/payloads";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import GroupAvatar from "../avatars/GroupAvatar";
import ExtraTile from "./ExtraTile";
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import { Action } from "../../../dispatcher/actions";
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import IconizedContextMenu, {
@ -44,7 +41,6 @@ import IconizedContextMenu, {
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import {
isMetaSpace,
@ -90,15 +86,11 @@ export const TAG_ORDER: TagID[] = [
DefaultTagID.Favourite,
DefaultTagID.DM,
DefaultTagID.Untagged,
// -- Custom Tags Placeholder --
DefaultTagID.LowPriority,
DefaultTagID.ServerNotice,
DefaultTagID.Suggested,
DefaultTagID.Archived,
];
const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority;
const ALWAYS_VISIBLE_TAGS: TagID[] = [
DefaultTagID.DM,
DefaultTagID.Untagged,
@ -274,9 +266,7 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
}}
/> }
<IconizedContextMenuOption
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
? _t("Explore community rooms")
: _t("Explore public rooms")}
label={_t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
@ -359,22 +349,9 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
},
};
function customTagAesthetics(tagId: TagID): ITagAesthetics {
if (tagId.startsWith("u.")) {
tagId = tagId.substring(2);
}
return {
sectionLabel: _td("Custom Tag"),
sectionLabelRaw: tagId,
isInvite: false,
defaultHidden: false,
};
}
@replaceableComponent("views.rooms.RoomList")
export default class RoomList extends React.PureComponent<IProps, IState> {
private dispatcherRef;
private customTagStoreRef;
private roomStoreToken: fbEmitter.EventSubscription;
private treeRef = createRef<HTMLDivElement>();
@ -396,7 +373,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
this.updateLists(); // trigger the first update
}
@ -404,7 +380,6 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef);
if (this.customTagStoreRef) this.customTagStoreRef.remove();
if (this.roomStoreToken) this.roomStoreToken.remove();
}
@ -463,12 +438,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private updateLists = () => {
const newLists = RoomListStore.instance.orderedLists;
const previousListIds = Object.keys(this.state.sublists);
const newListIds = Object.keys(newLists).filter(t => {
if (!isCustomTag(t)) return true; // always include non-custom tags
// if the tag is custom though, only include it if it is enabled
return CustomRoomTagStore.getTags()[t];
});
const newListIds = Object.keys(newLists);
const isNameFiltering = !!RoomListStore.instance.getFirstNameFilterCondition();
let doUpdate = this.state.isNameFiltering !== isNameFiltering || arrayHasDiff(previousListIds, newListIds);
@ -559,68 +529,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
});
}
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
if (SpaceStore.spacesEnabled) return [];
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/element-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {
return g.myMembership === 'invite';
}).map(g => {
const avatar = (
<GroupAvatar
groupId={g.groupId}
groupName={g.name}
groupAvatarUrl={g.avatarUrl}
width={32}
height={32}
resizeMethod='crop'
/>
);
const openGroup = () => {
defaultDispatcher.dispatch({
action: 'view_group',
group_id: g.groupId,
});
};
return (
<ExtraTile
isMinimized={this.props.isMinimized}
isSelected={false}
displayName={g.name}
avatar={avatar}
notificationState={StaticNotificationState.RED_EXCLAMATION}
onClick={openGroup}
key={`temporaryGroupTile_${g.groupId}`}
/>
);
});
}
private renderSublists(): React.ReactElement[] {
// show a skeleton UI if the user is in no rooms and they are not filtering and have no suggested rooms
const showSkeleton = !this.state.isNameFiltering && !this.state.suggestedRooms?.length &&
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
return TAG_ORDER.reduce((tags, tagId) => {
if (tagId === CUSTOM_TAGS_BEFORE_TAG) {
const customTags = Object.keys(this.state.sublists)
.filter(tagId => isCustomTag(tagId));
tags.push(...customTags);
}
tags.push(tagId);
return tags;
}, [] as TagID[])
return TAG_ORDER
.map(orderedTagId => {
let extraTiles = null;
if (orderedTagId === DefaultTagID.Invite) {
extraTiles = this.renderCommunityInvites();
} else if (orderedTagId === DefaultTagID.Suggested) {
if (orderedTagId === DefaultTagID.Suggested) {
extraTiles = this.renderSuggestedRooms();
}
const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
? customTagAesthetics(orderedTagId)
: TAG_AESTHETICS[orderedTagId];
const aesthetics = TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
let alwaysVisible = ALWAYS_VISIBLE_TAGS.includes(orderedTagId);

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ComponentProps, useContext, useEffect, useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { ClientEvent } from "matrix-js-sdk/src/client";
@ -30,7 +30,6 @@ import IconizedContextMenu, {
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import dis from "../../../dispatcher/dispatcher";
import {
shouldShowSpaceInvite,
showAddExistingRooms,
@ -38,14 +37,7 @@ import {
showCreateNewSubspace,
showSpaceInvite,
} from "../../../utils/space";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { ButtonEvent } from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import EditCommunityPrototypeDialog from "../dialogs/EditCommunityPrototypeDialog";
import { Action } from "../../../dispatcher/actions";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import ErrorDialog from "../dialogs/ErrorDialog";
import { showCommunityInviteDialog } from "../../../RoomInvite";
import { useDispatcher } from "../../../hooks/useDispatcher";
import InlineSpinner from "../elements/InlineSpinner";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
@ -57,7 +49,6 @@ import {
UPDATE_HOME_BEHAVIOUR,
UPDATE_SELECTED_SPACE,
} from "../../../stores/spaces";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import TooltipTarget from "../elements/TooltipTarget";
import { BetaPill } from "../beta/BetaCard";
import PosthogTrackers from "../../../PosthogTrackers";
@ -74,69 +65,6 @@ const contextMenuBelow = (elementRect: DOMRect) => {
return { left, top, chevronFace };
};
const PrototypeCommunityContextMenu = (props: ComponentProps<typeof SpaceContextMenu>) => {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
let settingsOption;
if (CommunityPrototypeStore.instance.isAdminOf(communityId)) {
const onCommunitySettingsClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
props.onFinished();
};
settingsOption = (
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")}
aria-label={_t("Community settings")}
onClick={onCommunitySettingsClick}
/>
);
}
const onCommunityMembersClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
// We'd ideally just pop open a right panel with the member list, but the current
// way the right panel is structured makes this exceedingly difficult. Instead, we'll
// switch to the general room and open the member list there as it should be in sync
// anyways.
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: chat.roomId,
metricsTrigger: undefined, // Deprecated groups
}, true);
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomMemberList }, undefined, chat.roomId);
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
title: _t('Failed to find the general chat for this community'),
description: _t("Failed to find the general chat for this community"),
});
}
props.onFinished();
};
return <IconizedContextMenu {...props} compact>
<IconizedContextMenuOptionList first>
{ settingsOption }
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconMembers"
label={_t("Members")}
onClick={onCommunityMembersClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
};
// Long-running actions that should trigger a spinner
enum PendingActionType {
JoinRoom,
@ -184,11 +112,10 @@ const usePendingActions = (): Map<PendingActionType, Set<string>> => {
};
interface IProps {
spacePanelDisabled: boolean;
onVisibilityChange?(): void;
}
const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
const RoomListHeader = ({ onVisibilityChange }: IProps) => {
const cli = useContext(MatrixClientContext);
const [mainMenuDisplayed, mainMenuHandle, openMainMenu, closeMainMenu] = useContextMenu<HTMLDivElement>();
const [plusMenuDisplayed, plusMenuHandle, openPlusMenu, closePlusMenu] = useContextMenu<HTMLDivElement>();
@ -226,11 +153,8 @@ const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
return <div className="mx_LeftPanel_roomListFilterCount">
{ _t("%(count)s results", { count }) }
</div>;
} else if (spacePanelDisabled) {
return null;
}
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
const canAddRooms = activeSpace?.currentState?.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
const canCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
@ -239,15 +163,13 @@ const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
// If the user can't do anything on the plus menu, don't show it. This aims to target the
// plus menu shown on the Home tab primarily: the user has options to use the menu for
// communities and spaces, but is at risk of no options on the Home tab.
const canShowPlusMenu = canCreateRooms || canExploreRooms || activeSpace || communityId;
const canShowPlusMenu = canCreateRooms || canExploreRooms || activeSpace;
let contextMenu: JSX.Element;
if (mainMenuDisplayed) {
let ContextMenuComponent;
if (activeSpace) {
ContextMenuComponent = SpaceContextMenu;
} else if (communityId) {
ContextMenuComponent = PrototypeCommunityContextMenu;
} else {
ContextMenuComponent = HomeButtonContextMenu;
}
@ -271,17 +193,6 @@ const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
closePlusMenu();
}}
/>;
} else if (CommunityPrototypeStore.instance.canInviteTo(communityId)) {
inviteOption = <IconizedContextMenuOption
iconClassName="mx_RoomListHeader_iconInvite"
label={_t("Invite")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
closePlusMenu();
}}
/>;
}
let createNewRoomOption: JSX.Element;
@ -413,8 +324,6 @@ const RoomListHeader = ({ spacePanelDisabled, onVisibilityChange }: IProps) => {
let title: string;
if (activeSpace) {
title = spaceName;
} else if (communityId) {
title = CommunityPrototypeStore.instance.getSelectedCommunityName();
} else {
title = getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
}

View file

@ -27,8 +27,6 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import SdkConfig from "../../../SdkConfig";
import IdentityAuthClient from '../../../IdentityAuthClient';
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import InviteReason from "../elements/InviteReason";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
@ -116,7 +114,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
componentDidMount() {
this.checkInvitedEmail();
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
}
componentDidUpdate(prevProps, prevState) {
@ -125,10 +122,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
}
componentWillUnmount() {
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
}
private async checkInvitedEmail() {
// If this is an invite and we've been told what email address was
// invited, fetch the user's account emails and discovery bindings so we
@ -163,13 +156,6 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
}
private onCommunityUpdate = (roomId: string): void => {
if (this.props.room && this.props.room.roomId !== roomId) {
return;
}
this.forceUpdate(); // we have nothing to update
};
private getMessageCase(): MessageCase {
const isGuest = MatrixClientPeg.get().isGuest();
@ -241,15 +227,8 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
}
private communityProfile(): { displayName?: string, avatarMxc?: string } {
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
return { displayName: null, avatarMxc: null };
}
private roomName(atStart = false): string {
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
const profile = this.communityProfile();
if (profile.displayName) name = profile.displayName;
const name = this.props.room ? this.props.room.name : this.props.roomAlias;
if (name) {
return name;
} else if (atStart) {
@ -465,10 +444,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
break;
}
case MessageCase.Invite: {
const oobData = Object.assign({}, this.props.oobData, {
avatarUrl: this.communityProfile().avatarMxc,
});
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
const inviteMember = this.getInviteMember();
let inviterElement;

View file

@ -52,7 +52,6 @@ import IconizedContextMenu, {
IconizedContextMenuOptionList,
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
import VoiceChannelStore, { VoiceChannelEvent, IJitsiParticipant } from "../../../stores/VoiceChannelStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import PosthogTrackers from "../../../PosthogTrackers";
@ -158,14 +157,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(prevProps.room?.roomId),
this.onCommunityUpdate,
);
CommunityPrototypeStore.instance.on(
CommunityPrototypeStore.getUpdateEventName(this.props.room?.roomId),
this.onCommunityUpdate,
);
prevProps.room?.off(RoomEvent.Name, this.onRoomNameUpdate);
this.props.room?.on(RoomEvent.Name, this.onRoomNameUpdate);
}
@ -186,10 +177,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.notificationState.on(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
this.props.room?.on(RoomEvent.Name, this.onRoomNameUpdate);
CommunityPrototypeStore.instance.on(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
);
}
public componentWillUnmount() {
@ -199,20 +186,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
this.onRoomPreviewChanged,
);
CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
);
this.props.room.off(RoomEvent.Name, this.onRoomNameUpdate);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
defaultDispatcher.unregister(this.dispatcherRef);
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CommunityPrototypeStore.instance.off(
CommunityPrototypeStore.getUpdateEventName(this.props.room.roomId),
this.onCommunityUpdate,
);
}
private onAction = (payload: ActionPayload) => {
@ -226,11 +205,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
};
private onCommunityUpdate = (roomId: string) => {
if (roomId !== this.props.room.roomId) return;
this.forceUpdate(); // we don't have anything to actually update
};
private onRoomPreviewChanged = (room: Room) => {
if (this.props.room && room.roomId === this.props.room.roomId) {
this.generatePreview();
@ -667,12 +641,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
'mx_RoomTile_minimized': this.props.isMinimized,
});
let roomProfile: IRoomProfile = { displayName: null, avatarMxc: null };
if (this.props.tag === DefaultTagID.Invite) {
roomProfile = CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
}
let name = roomProfile.displayName || this.props.room.name;
let name = this.props.room.name;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
@ -797,7 +766,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
room={this.props.room}
avatarSize={32}
displayBadge={this.props.isMinimized}
oobData={({ avatarUrl: roomProfile.avatarMxc })}
tooltipProps={{ tabIndex: isActive ? 0 : -1 }}
/>
<div className="mx_RoomTile_details">

View file

@ -21,7 +21,6 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import DateSeparator from "../messages/DateSeparator";
@ -69,7 +68,6 @@ export default class SearchResultTile extends React.Component<IProps> {
const layout = SettingsStore.getValue("layout");
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
const enableFlair = SettingsStore.getValue(UIFeature.Flair);
const timeline = result.context.getTimeline();
for (let j = 0; j < timeline.length; j++) {
@ -121,7 +119,6 @@ export default class SearchResultTile extends React.Component<IProps> {
onHeightChanged={this.props.onHeightChanged}
isTwelveHour={isTwelveHour}
alwaysShowTimestamps={alwaysShowTimestamps}
enableFlair={enableFlair}
lastInSection={lastInSection}
continuation={continuation}
callEventGrouper={this.callEventGroupers.get(mxEv.getContent().call_id)}

View file

@ -31,7 +31,6 @@ import RoomName from "../elements/RoomName";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ErrorDialog from '../dialogs/ErrorDialog';
import AccessibleButton from '../elements/AccessibleButton';
import SpaceStore from "../../../stores/spaces/SpaceStore";
interface IProps {
event: MatrixEvent;
@ -138,7 +137,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
}
let scopeHeader;
if (SpaceStore.spacesEnabled && this.room.isSpaceRoom()) {
if (this.room.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={this.room} height={32} width={32} />
<RoomName room={this.room} />

View file

@ -25,7 +25,6 @@ import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
import RelatedGroupSettings from "../../../room_settings/RelatedGroupSettings";
import AliasSettings from "../../../room_settings/AliasSettings";
import PosthogTrackers from "../../../../../PosthogTrackers";
@ -67,27 +66,10 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client);
const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');
const canChangeGroups = room.currentState.mayClientSendStateEvent("m.room.related_groups", client);
const groupsEvent = room.currentState.getStateEvents("m.room.related_groups", "");
const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ?
<UrlPreviewSettings room={room} /> :
null;
let flairSection;
if (SettingsStore.getValue(UIFeature.Flair)) {
flairSection = <>
<span className='mx_SettingsTab_subheading'>{ _t("Flair") }</span>
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
<RelatedGroupSettings
roomId={room.roomId}
canSetRelatedGroups={canChangeGroups}
relatedGroupsEvent={groupsEvent}
/>
</div>
</>;
}
let leaveSection;
if (room.getMyMembership() === "join") {
leaveSection = <>
@ -115,7 +97,6 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
canonicalAliasEvent={canonicalAliasEv}
/>
<div className="mx_SettingsTab_heading">{ _t("Other") }</div>
{ flairSection }
{ urlPreviewSettings }
{ leaveSection }
</div>

View file

@ -1,35 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { _t } from "../../../../../languageHandler";
import GroupUserSettings from "../../../groups/GroupUserSettings";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
@replaceableComponent("views.settings.tabs.user.FlairUserSettingsTab")
export default class FlairUserSettingsTab extends React.Component {
render() {
return (
<div className="mx_SettingsTab">
<span className="mx_SettingsTab_heading">{ _t("Flair") }</span>
<div className="mx_SettingsTab_section">
<GroupUserSettings />
</div>
</div>
);
}
}

View file

@ -243,7 +243,7 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
) }
{ _t("Debug logs contain application " +
"usage data including your username, the IDs or aliases of " +
"the rooms or groups you have visited, which UI elements you " +
"the rooms you have visited, which UI elements you " +
"last interacted with, and the usernames of other users. " +
"They do not contain messages.",
) }

View file

@ -15,8 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useContext, useEffect, useState } from 'react';
import { EventType } from 'matrix-js-sdk/src/@types/event';
import React from 'react';
import { _t } from "../../../../../languageHandler";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
@ -27,18 +26,10 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
import SettingsFlag from '../../../elements/SettingsFlag';
import AccessibleButton from "../../../elements/AccessibleButton";
import GroupAvatar from "../../../avatars/GroupAvatar";
import dis from "../../../../../dispatcher/dispatcher";
import GroupActions from "../../../../../actions/GroupActions";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import { useDispatcher } from "../../../../../hooks/useDispatcher";
import { createSpaceFromCommunity } from "../../../../../utils/space";
import Spinner from "../../../elements/Spinner";
import { UserTab } from "../../../dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
import { Action } from "../../../../../dispatcher/actions";
import { ViewRoomPayload } from "../../../../../dispatcher/payloads/ViewRoomPayload";
import { CreateEventField, IGroupSummary } from '../../../../../@types/groups';
interface IProps {
closeSettingsFn(success: boolean): void;
@ -58,85 +49,6 @@ interface IState {
readMarkerOutOfViewThresholdMs: string;
}
type Community = IGroupSummary & {
groupId: string;
spaceId?: string;
};
const CommunityMigrator = ({ onFinished }) => {
const cli = useContext(MatrixClientContext);
const [communities, setCommunities] = useState<Community[]>(null);
useEffect(() => {
dis.dispatch(GroupActions.fetchJoinedGroups(cli));
}, [cli]);
useDispatcher(dis, async payload => {
if (payload.action === "GroupActions.fetchJoinedGroups.success") {
const communities: Community[] = [];
const migratedSpaceMap = new Map(cli.getRooms().map(room => {
const createContent = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent();
if (createContent?.[CreateEventField]) {
return [createContent[CreateEventField], room.roomId] as [string, string];
}
}).filter(Boolean));
for (const groupId of payload.result.groups) {
const summary = await cli.getGroupSummary(groupId) as IGroupSummary;
if (summary.user.is_privileged) {
communities.push({
...summary,
groupId,
spaceId: migratedSpaceMap.get(groupId),
});
}
}
setCommunities(communities);
}
});
if (!communities) {
return <Spinner />;
}
return <div className="mx_PreferencesUserSettingsTab_CommunityMigrator">
{ communities.map(community => (
<div key={community.groupId}>
<GroupAvatar
groupId={community.groupId}
groupAvatarUrl={community.profile.avatar_url}
groupName={community.profile.name}
width={32}
height={32}
/>
{ community.profile.name }
<AccessibleButton
kind="primary_outline"
onClick={() => {
if (community.spaceId) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: community.spaceId,
metricsTrigger: undefined, // other
});
onFinished();
} else {
createSpaceFromCommunity(cli, community.groupId).then(([spaceId]) => {
if (spaceId) {
community.spaceId = spaceId;
setCommunities([...communities]); // force component re-render
}
});
}
}}
>
{ community.spaceId ? _t("Open Space") : _t("Create Space") }
</AccessibleButton>
</div>
)) }
</div>;
};
@replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab")
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
static ROOM_LIST_SETTINGS = [
@ -147,10 +59,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
"Spaces.allRoomsInHome",
];
static COMMUNITIES_SETTINGS = [
"showCommunitiesInsteadOfSpaces",
];
static KEYBINDINGS_SETTINGS = [
'ctrlFForSearch',
];
@ -193,7 +101,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
'scrollToBottomOnMessageSent',
];
static GENERAL_SETTINGS = [
'TagPanel.enableTagPanel',
'promptBeforeInviteUnknownUsers',
// Start automatically after startup (electron-only)
// Autocomplete delay (niche text box)
@ -356,19 +263,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
<p>{ _t("Communities have been archived to make way for Spaces but you can convert your " +
"communities into Spaces below. Converting will ensure your conversations get the latest " +
"features.") }</p>
<details>
<summary>{ _t("Show my Communities") }</summary>
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
</details>
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS, SettingLevel.DEVICE) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Keyboard shortcuts") }</span>
<div className="mx_SettingsFlag">

View file

@ -35,9 +35,6 @@ import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal";
import GenericFeatureFeedbackDialog from "../dialogs/GenericFeatureFeedbackDialog";
import SettingsStore from "../../../settings/SettingsStore";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserSettingsDialog";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@ -267,14 +264,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
let body;
if (visibility === null) {
const onCreateSpaceFromCommunityClick = () => {
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Preferences,
});
onFinished();
};
body = <React.Fragment>
<h2>{ _t("Create a space") }</h2>
<p>
@ -296,12 +285,6 @@ const SpaceCreateMenu = ({ onFinished }) => {
/>
<p>
{ _t("You can also make Spaces from <a>communities</a>.", {}, {
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
{ sub }
</AccessibleButton>,
}) }
<br />
{ _t("To join a space you'll need an invite.") }
</p>

View file

@ -38,7 +38,6 @@ import * as Rooms from "./Rooms";
import DMRoomMap from "./utils/DMRoomMap";
import { getAddressType } from "./UserAddress";
import { getE2EEWellKnown } from "./utils/WellKnownUtils";
import GroupStore from "./stores/GroupStore";
import { isJoinedOrNearlyJoined } from "./utils/membership";
import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler";
import SpaceStore from "./stores/spaces/SpaceStore";
@ -60,7 +59,6 @@ export interface IOpts {
encryption?: boolean;
inlineErrors?: boolean;
andView?: boolean;
associatedWithCommunity?: string;
avatar?: File | string; // will upload if given file, else mxcUrl is needed
roomType?: RoomType | string;
historyVisibility?: HistoryVisibility;
@ -245,9 +243,6 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
if (opts.parentSpace) {
return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], opts.suggested);
}
if (opts.associatedWithCommunity) {
return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false);
}
}).then(() => {
// Set up voice rooms with a Jitsi widget
if (opts.roomType === RoomType.UnstableCall) {

45
src/group_helpers.tsx Normal file
View file

@ -0,0 +1,45 @@
/*
Copyright 2022 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 * as React from "react";
import Modal from "./Modal";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import { _t } from "./languageHandler";
import SdkConfig, { DEFAULTS } from "./SdkConfig";
export function showGroupReplacedWithSpacesDialog(groupId: string) {
const learnMoreUrl = SdkConfig.get().spaces_learn_more_url ?? DEFAULTS.spaces_learn_more_url;
Modal.createTrackedDialog("Groups are now Spaces", '', QuestionDialog, {
title: _t("That link is no longer supported"),
description: <>
<p>
{ _t(
"You're trying to access a community link (%(groupId)s).<br/>" +
"Communities are no longer supported and have been replaced by spaces.<br2/>" +
"<a>Learn more about spaces here.</a>",
{ groupId },
{
br: () => <br />,
br2: () => <br />,
a: (sub) => <a href={learnMoreUrl} rel="noreferrer noopener" target="_blank">{ sub }</a>,
},
) }
</p>
</>,
hasCancelButton: false,
});
}

View file

@ -1,57 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import PropTypes from 'prop-types';
import { _t } from './languageHandler';
export const GroupMemberType = PropTypes.shape({
userId: PropTypes.string.isRequired,
displayname: PropTypes.string,
avatarUrl: PropTypes.string,
});
export const GroupRoomType = PropTypes.shape({
displayname: PropTypes.string,
name: PropTypes.string,
roomId: PropTypes.string.isRequired,
canonicalAlias: PropTypes.string,
avatarUrl: PropTypes.string,
});
export function groupMemberFromApiObject(apiObject) {
return {
userId: apiObject.user_id,
displayname: apiObject.displayname,
avatarUrl: apiObject.avatar_url,
isPrivileged: apiObject.is_privileged,
};
}
export function groupRoomFromApiObject(apiObject) {
return {
displayname: apiObject.name || apiObject.canonical_alias || _t("Unnamed Room"),
name: apiObject.name,
roomId: apiObject.room_id,
canonicalAlias: apiObject.canonical_alias,
avatarUrl: apiObject.avatar_url,
topic: apiObject.topic,
numJoinedMembers: apiObject.num_joined_members,
worldReadable: apiObject.world_readable,
guestCanJoin: apiObject.guest_can_join,
isPublic: apiObject.is_public !== false,
};
}

View file

@ -31,7 +31,7 @@
"Our complete cookie policy can be found <CookiePolicyLink>here</CookiePolicyLink>.": "Our complete cookie policy can be found <CookiePolicyLink>here</CookiePolicyLink>.",
"Analytics": "Analytics",
"Some examples of the information being sent to us to help make %(brand)s better includes:": "Some examples of the information being sent to us to help make %(brand)s better includes:",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
"Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user ID, that data is removed before being sent to the server.",
"Error": "Error",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
"Dismiss": "Dismiss",
@ -107,21 +107,8 @@
"%(value)sh": "%(value)sh",
"%(value)sm": "%(value)sm",
"%(value)ss": "%(value)ss",
"Who would you like to add to this community?": "Who would you like to add to this community?",
"Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID",
"Invite new community members": "Invite new community members",
"Name or Matrix ID": "Name or Matrix ID",
"Invite to Community": "Invite to Community",
"Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?",
"Show these rooms to non-members on the community page and room list?": "Show these rooms to non-members on the community page and room list?",
"Add rooms to the community": "Add rooms to the community",
"Room name or address": "Room name or address",
"Add to community": "Add to community",
"Failed to invite the following users to %(groupId)s:": "Failed to invite the following users to %(groupId)s:",
"Failed to invite users to community": "Failed to invite users to community",
"Failed to invite users to %(groupId)s": "Failed to invite users to %(groupId)s",
"Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:",
"Unnamed Room": "Unnamed Room",
"That link is no longer supported": "That link is no longer supported",
"You're trying to access a community link (%(groupId)s).<br/>Communities are no longer supported and have been replaced by spaces.<br2/><a>Learn more about spaces here.</a>": "You're trying to access a community link (%(groupId)s).<br/>Communities are no longer supported and have been replaced by spaces.<br2/><a>Learn more about spaces here.</a>",
"Identity server has no terms of service": "Identity server has no terms of service",
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.",
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
@ -539,9 +526,6 @@
"%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s has allowed guests to join the room.",
"%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s has prevented guests from joining the room.",
"%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s changed guest access to %(rule)s",
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
"%(senderDisplayName)s set the server ACLs for this room.": "%(senderDisplayName)s set the server ACLs for this room.",
"%(senderDisplayName)s changed the server ACLs for this room.": "%(senderDisplayName)s changed the server ACLs for this room.",
"🎉 All servers are banned from participating! This room can no longer be used.": "🎉 All servers are banned from participating! This room can no longer be used.",
@ -875,11 +859,9 @@
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
"Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.",
"Message Pinning": "Message Pinning",
"Threaded messaging": "Threaded messaging",
"Custom user status messages": "Custom user status messages",
"Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)",
"Voice & video rooms (under active development)": "Voice & video rooms (under active development)",
"Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers (requires manual setup)": "Multiple integration managers (requires manual setup)",
@ -937,7 +919,6 @@
"Surround selected text when typing special characters": "Surround selected text when typing special characters",
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
"Mirror local video feed": "Mirror local video feed",
"Enable Community Filter Panel": "Enable Community Filter Panel",
"Match system theme": "Match system theme",
"Use a system font": "Use a system font",
"System font name": "System font name",
@ -965,8 +946,6 @@
"Show chat effects (animations when receiving e.g. confetti)": "Show chat effects (animations when receiving e.g. confetti)",
"Show all rooms in Home": "Show all rooms in Home",
"All rooms you're in will appear in Home.": "All rooms you're in will appear in Home.",
"Display Communities instead of Spaces": "Display Communities instead of Spaces",
"Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.": "Temporarily show communities instead of Spaces for this session. Support for this will be removed in the near future. This will reload Element.",
"Developer mode": "Developer mode",
"Automatically send debug logs on any error": "Automatically send debug logs on any error",
"Automatically send debug logs on decryption errors": "Automatically send debug logs on decryption errors",
@ -1152,7 +1131,6 @@
"Open space for anyone, best for communities": "Open space for anyone, best for communities",
"Private": "Private",
"Invite only, best for yourself or teams": "Invite only, best for yourself or teams",
"You can also make Spaces from <a>communities</a>.": "You can also make Spaces from <a>communities</a>.",
"To join a space you'll need an invite.": "To join a space you'll need an invite.",
"Go back": "Go back",
"Your public space": "Your public space",
@ -1413,7 +1391,6 @@
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
"Customise your appearance": "Customise your appearance",
"Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.",
"Flair": "Flair",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success",
"Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them",
@ -1438,7 +1415,7 @@
"Chat with %(brand)s Bot": "Chat with %(brand)s Bot",
"Bug reporting": "Bug reporting",
"If you've submitted a bug via GitHub, debug logs can help us track down the problem. ": "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ",
"Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.": "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.",
"Debug logs contain application usage data including your 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.": "Debug logs contain application usage data including your 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.",
"Submit debug logs": "Submit debug logs",
"To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.": "To report a Matrix-related security issue, please read the Matrix.org <a>Security Disclosure Policy</a>.",
"Help & About": "Help & About",
@ -1486,18 +1463,12 @@
"If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.",
"Room ID or address of ban list": "Room ID or address of ban list",
"Subscribe": "Subscribe",
"Open Space": "Open Space",
"Create Space": "Create Space",
"Start automatically after system login": "Start automatically after system login",
"Warn before quitting": "Warn before quitting",
"Always show the window menu bar": "Always show the window menu bar",
"Show tray icon and minimise window to it on close": "Show tray icon and minimise window to it on close",
"Preferences": "Preferences",
"Room list": "Room list",
"Communities": "Communities",
"Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.": "Communities have been archived to make way for Spaces but you can convert your communities into Spaces below. Converting will ensure your conversations get the latest features.",
"Show my Communities": "Show my Communities",
"If a community isn't shown you may not have permission to convert it.": "If a community isn't shown you may not have permission to convert it.",
"Keyboard shortcuts": "Keyboard shortcuts",
"To view all keyboard shortcuts, <a>click here</a>.": "To view all keyboard shortcuts, <a>click here</a>.",
"Displaying time": "Displaying time",
@ -1699,7 +1670,6 @@
"and %(count)s others...|other": "and %(count)s others...",
"and %(count)s others...|one": "and one other...",
"Invite to this room": "Invite to this room",
"Invite to this community": "Invite to this community",
"Invite to this space": "Invite to this space",
"Invited": "Invited",
"Filter room members": "Filter room members",
@ -1786,7 +1756,6 @@
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
"Add existing room": "Add existing room",
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
"Explore community rooms": "Explore community rooms",
"Explore public rooms": "Explore public rooms",
"Add room": "Add room",
"Invites": "Invites",
@ -1794,14 +1763,10 @@
"System Alerts": "System Alerts",
"Historical": "Historical",
"Suggested Rooms": "Suggested Rooms",
"Custom Tag": "Custom Tag",
"Empty room": "Empty room",
"Can't see what you're looking for?": "Can't see what you're looking for?",
"Start a new chat": "Start a new chat",
"Explore all public rooms": "Explore all public rooms",
"Community settings": "Community settings",
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
"Members": "Members",
"%(count)s results|other": "%(count)s results",
"%(count)s results|one": "%(count)s result",
"Invite": "Invite",
@ -1930,13 +1895,6 @@
"Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)",
"Show more": "Show more",
"Error updating flair": "Error updating flair",
"There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.": "There was an error updating the flair for this room. The server may not allow it or a temporary error occurred.",
"Invalid community ID": "Invalid community ID",
"'%(groupId)s' is not a valid community ID": "'%(groupId)s' is not a valid community ID",
"Showing flair for these communities:": "Showing flair for these communities:",
"This room is not showing flair for any communities": "This room is not showing flair for any communities",
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
"Room Name": "Room Name",
"Room Topic": "Room Topic",
"Room avatar": "Room avatar",
@ -2023,11 +1981,6 @@
"Failed to mute user": "Failed to mute user",
"Unmute": "Unmute",
"Mute": "Mute",
"Remove from community": "Remove from community",
"Disinvite this user from community?": "Disinvite this user from community?",
"Remove this user from community?": "Remove this user from community?",
"Failed to withdraw invitation": "Failed to withdraw invitation",
"Failed to remove user from community": "Failed to remove user from community",
"Failed to change power level": "Failed to change power level",
"You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.",
"Are you sure?": "Are you sure?",
@ -2202,23 +2155,6 @@
"My live location": "My live location",
"Drop a Pin": "Drop a Pin",
"What location type do you want to share?": "What location type do you want to share?",
"Failed to load group members": "Failed to load group members",
"Filter community members": "Filter community members",
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
"Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.",
"Failed to remove room from community": "Failed to remove room from community",
"Failed to remove '%(roomName)s' from %(groupId)s": "Failed to remove '%(roomName)s' from %(groupId)s",
"Something went wrong!": "Something went wrong!",
"The visibility of '%(roomName)s' in %(groupId)s could not be updated.": "The visibility of '%(roomName)s' in %(groupId)s could not be updated.",
"Visibility in Room List": "Visibility in Room List",
"Visible to everyone": "Visible to everyone",
"Only visible to community members": "Only visible to community members",
"Add rooms to this community": "Add rooms to this community",
"Filter community rooms": "Filter community rooms",
"Something went wrong when trying to get your communities.": "Something went wrong when trying to get your communities.",
"Loading...": "Loading...",
"Display your community flair in rooms configured to show it.": "Display your community flair in rooms configured to show it.",
"You're not currently a member of any communities.": "You're not currently a member of any communities.",
"Frequently Used": "Frequently Used",
"Smileys & People": "Smileys & People",
"Animals & Nature": "Animals & Nature",
@ -2245,6 +2181,7 @@
"Widgets do not use message encryption.": "Widgets do not use message encryption.",
"Widget added by": "Widget added by",
"This widget may use cookies.": "This widget may use cookies.",
"Loading...": "Loading...",
"Error loading Widget": "Error loading Widget",
"Error - Mixed content": "Error - Mixed content",
"Popout widget": "Popout widget",
@ -2259,6 +2196,7 @@
"Share content": "Share content",
"Backspace": "Backspace",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"Something went wrong!": "Something went wrong!",
"%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s",
"%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times",
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined",
@ -2413,14 +2351,6 @@
"Create a new room": "Create a new room",
"Search for rooms": "Search for rooms",
"Adding spaces has moved.": "Adding spaces has moved.",
"Matrix ID": "Matrix ID",
"Matrix Room ID": "Matrix Room ID",
"email address": "email address",
"That doesn't look like a valid email address": "That doesn't look like a valid email address",
"You have entered an invalid address.": "You have entered an invalid address.",
"Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.",
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.",
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Use an identity server to invite by email. Manage in <settings>Settings</settings>.",
"You can read all our terms <PrivacyPolicyUrl>here</PrivacyPolicyUrl>": "You can read all our terms <PrivacyPolicyUrl>here</PrivacyPolicyUrl>",
"Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.": "Help us identify issues and improve Element by sharing anonymous usage data. To understand how people use multiple devices, we'll generate a random identifier, shared by your devices.",
"We <Bold>don't</Bold> record or profile any account data": "We <Bold>don't</Bold> record or profile any account data",
@ -2459,15 +2389,6 @@
"Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
"Unavailable": "Unavailable",
"Changelog": "Changelog",
"Email address": "Email address",
"Add another email": "Add another email",
"People you know on %(brand)s": "People you know on %(brand)s",
"Hide": "Hide",
"Show": "Show",
"Skip": "Skip",
"Send %(count)s invites|other": "Send %(count)s invites",
"Send %(count)s invites|one": "Send %(count)s invite",
"Invite people to join %(communityName)s": "Invite people to join %(communityName)s",
"You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)",
"Removing…": "Removing…",
"Confirm Removal": "Confirm Removal",
@ -2476,24 +2397,7 @@
"Clear all data in this session?": "Clear all data in this session?",
"Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.",
"Clear all data": "Clear all data",
"There was an error creating your community. The name may be taken or the server is unable to process your request.": "There was an error creating your community. The name may be taken or the server is unable to process your request.",
"Community ID: +<localpart />:%(domain)s": "Community ID: +<localpart />:%(domain)s",
"Use this when referencing your community to others. The community ID cannot be changed.": "Use this when referencing your community to others. The community ID cannot be changed.",
"You can change this later if needed.": "You can change this later if needed.",
"What's the name of your community or team?": "What's the name of your community or team?",
"Enter name": "Enter name",
"Add image (optional)": "Add image (optional)",
"An image will help people identify your community.": "An image will help people identify your community.",
"Community IDs cannot be empty.": "Community IDs cannot be empty.",
"Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community IDs may only contain characters a-z, 0-9, or '=_-./'",
"Something went wrong whilst creating your community": "Something went wrong whilst creating your community",
"Create Community": "Create Community",
"Community Name": "Community Name",
"Example": "Example",
"Community ID": "Community ID",
"example": "example",
"Please enter a name for the room": "Please enter a name for the room",
"Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.",
"Everyone in <SpaceName/> will be able to find and join this room.": "Everyone in <SpaceName/> will be able to find and join this room.",
"You can change this at any time from room settings.": "You can change this at any time from room settings.",
"Anyone will be able to find and join this room, not just members of <SpaceName/>.": "Anyone will be able to find and join this room, not just members of <SpaceName/>.",
@ -2505,7 +2409,6 @@
"You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.",
"You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.",
"Create a room": "Create a room",
"Create a room in %(communityName)s": "Create a room in %(communityName)s",
"Create a public room": "Create a public room",
"Create a private room": "Create a private room",
"Room type": "Room type",
@ -2519,26 +2422,13 @@
"Visible to space members": "Visible to space members",
"Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.",
"Create Room": "Create Room",
"This community has been upgraded into a Space": "This community has been upgraded into a Space",
"To view Spaces, hide communities in <a>Preferences</a>": "To view Spaces, hide communities in <a>Preferences</a>",
"Space created": "Space created",
"<SpaceName/> has been made and everyone who was a part of the community has been invited to it.": "<SpaceName/> has been made and everyone who was a part of the community has been invited to it.",
"To create a Space from another community, just pick the community in Preferences.": "To create a Space from another community, just pick the community in Preferences.",
"Failed to migrate community": "Failed to migrate community",
"Fetching data...": "Fetching data...",
"Creating Space...": "Creating Space...",
"Create Space from community": "Create Space from community",
"A link to the Space will be put in your community description.": "A link to the Space will be put in your community description.",
"All rooms will be added and all community members will be invited.": "All rooms will be added and all community members will be invited.",
"Flair won't be available in Spaces for the foreseeable future.": "Flair won't be available in Spaces for the foreseeable future.",
"This description will be shown to people when they view your space": "This description will be shown to people when they view your space",
"Space visibility": "Space visibility",
"Private space (invite only)": "Private space (invite only)",
"Public space": "Public space",
"Anyone in <SpaceName/> will be able to find and join.": "Anyone in <SpaceName/> will be able to find and join.",
"Anyone will be able to find and join this space, not just members of <SpaceName/>.": "Anyone will be able to find and join this space, not just members of <SpaceName/>.",
"Only people invited will be able to find and join this space.": "Only people invited will be able to find and join this space.",
"Add a space to a space you manage.": "Add a space to a space you manage.",
"Space visibility": "Space visibility",
"Private space (invite only)": "Private space (invite only)",
"Public space": "Public space",
"Want to add an existing space instead?": "Want to add an existing space instead?",
"Adding...": "Adding...",
"Sign out": "Sign out",
@ -2599,8 +2489,6 @@
"Edit Values": "Edit Values",
"Toolbox": "Toolbox",
"Developer Tools": "Developer Tools",
"There was an error updating your community. The server is unable to process your request.": "There was an error updating your community. The server is unable to process your request.",
"Update community": "Update community",
"The poll has ended. No votes were cast.": "The poll has ended. No votes were cast.",
"The poll has ended. Top answer: %(topAnswer)s": "The poll has ended. Top answer: %(topAnswer)s",
"Failed to end poll": "Failed to end poll",
@ -2680,16 +2568,17 @@
"The following users might not exist or are invalid, and cannot be invited: %(csvNames)s": "The following users might not exist or are invalid, and cannot be invited: %(csvNames)s",
"Recent Conversations": "Recent Conversations",
"Suggestions": "Suggestions",
"May include members not in %(communityName)s": "May include members not in %(communityName)s",
"Recently Direct Messaged": "Recently Direct Messaged",
"Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.": "Use an identity server to invite by email. <default>Use the default (%(defaultIdentityServerName)s)</default> or manage in <settings>Settings</settings>.",
"Use an identity server to invite by email. Manage in <settings>Settings</settings>.": "Use an identity server to invite by email. Manage in <settings>Settings</settings>.",
"Start a conversation with someone using their name, email address or username (like <userId/>).": "Start a conversation with someone using their name, email address or username (like <userId/>).",
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
"Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.",
"If you can't see who you're looking for, send them your invite link below.": "If you can't see who you're looking for, send them your invite link below.",
"Or send invite link": "Or send invite link",
"Unnamed Space": "Unnamed Space",
"Invite to %(roomName)s": "Invite to %(roomName)s",
"Unnamed Room": "Unnamed Room",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
@ -2829,11 +2718,12 @@
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.",
"Verification Pending": "Verification Pending",
"Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.",
"Email address": "Email address",
"This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.",
"Skip": "Skip",
"Share Room": "Share Room",
"Link to most recent message": "Link to most recent message",
"Share User": "Share User",
"Share Community": "Share Community",
"Share Room Message": "Share Room Message",
"Link to selected message": "Link to selected message",
"Link to room": "Link to room",
@ -2937,9 +2827,6 @@
"If you've forgotten your Security Key you can <button>set up new recovery options</button>": "If you've forgotten your Security Key you can <button>set up new recovery options</button>",
"Resume": "Resume",
"Hold": "Hold",
"Reject invitation": "Reject invitation",
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
"Unable to reject invite": "Unable to reject invite",
"Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)",
"Open in OpenStreetMap": "Open in OpenStreetMap",
"Forward": "Forward",
@ -2955,9 +2842,6 @@
"Space": "Space",
"Space home": "Space home",
"Manage & explore rooms": "Manage & explore rooms",
"Move up": "Move up",
"Move down": "Move down",
"View Community": "View Community",
"Thread options": "Thread options",
"Unable to start audio streaming.": "Unable to start audio streaming.",
"Failed to start livestream": "Failed to start livestream",
@ -3039,53 +2923,6 @@
"You must join the room to see its files": "You must join the room to see its files",
"No files visible in this room": "No files visible in this room",
"Attach files from chat or just drag and drop them anywhere in a room.": "Attach files from chat or just drag and drop them anywhere in a room.",
"Create community": "Create community",
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n": "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even add images with Matrix URLs <img src=\"mxc://url\" />\n</p>\n",
"Add rooms to the community summary": "Add rooms to the community summary",
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
"Add to summary": "Add to summary",
"Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:",
"Add a Room": "Add a Room",
"Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s",
"The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.",
"Add users to the community summary": "Add users to the community summary",
"Who would you like to add to this summary?": "Who would you like to add to this summary?",
"Failed to add the following users to the summary of %(groupId)s:": "Failed to add the following users to the summary of %(groupId)s:",
"Add a User": "Add a User",
"Failed to remove a user from the summary of %(groupId)s": "Failed to remove a user from the summary of %(groupId)s",
"The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary.",
"Failed to upload image": "Failed to upload image",
"Failed to update community": "Failed to update community",
"Unable to accept invite": "Unable to accept invite",
"Unable to join community": "Unable to join community",
"You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.": "You are an administrator of this community. You will not be able to rejoin without an invite from another administrator.",
"Leave Community": "Leave Community",
"Leave %(groupName)s?": "Leave %(groupName)s?",
"Unable to leave community": "Unable to leave community",
"Community Settings": "Community Settings",
"Want more than a community? <a>Get your own server</a>": "Want more than a community? <a>Get your own server</a>",
"Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.",
"You can create a Space from this community <a>here</a>.": "You can create a Space from this community <a>here</a>.",
"Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.": "Ask the <a>admins</a> of this community to make it into a Space and keep a look out for the invite.",
"Communities can now be made into Spaces": "Communities can now be made into Spaces",
"Spaces are a new way to make a community, with new features coming.": "Spaces are a new way to make a community, with new features coming.",
"Communities won't receive further updates.": "Communities won't receive further updates.",
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.",
"Featured Rooms:": "Featured Rooms:",
"Featured Users:": "Featured Users:",
"%(inviter)s has invited you to join this community": "%(inviter)s has invited you to join this community",
"Join this community": "Join this community",
"Leave this community": "Leave this community",
"You are an administrator of this community": "You are an administrator of this community",
"You are a member of this community": "You are a member of this community",
"Who can join this community?": "Who can join this community?",
"Only people who have been invited": "Only people who have been invited",
"Everyone": "Everyone",
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
"Long Description (HTML)": "Long Description (HTML)",
"Community %(groupId)s not found": "Community %(groupId)s not found",
"This homeserver does not support communities": "This homeserver does not support communities",
"Failed to load %(groupId)s": "Failed to load %(groupId)s",
"Great, that'll help people know it's you": "Great, that'll help people know it's you",
"Add a photo so people know it's you.": "Add a photo so people know it's you.",
"Welcome %(name)s": "Welcome %(name)s",
@ -3096,16 +2933,12 @@
"Explore Public Rooms": "Explore Public Rooms",
"Create a Group Chat": "Create a Group Chat",
"Open dial pad": "Open dial pad",
"Public community": "Public community",
"Private community": "Private community",
"To view %(communityName)s, swap to communities in your <a>preferences</a>": "To view %(communityName)s, swap to communities in your <a>preferences</a>",
"To join %(communityName)s, swap to communities in your <a>preferences</a>": "To join %(communityName)s, swap to communities in your <a>preferences</a>",
"Wait!": "Wait!",
"If someone told you to copy/paste something here, there is a high likelihood you're being scammed!": "If someone told you to copy/paste something here, there is a high likelihood you're being scammed!",
"If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!": "If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!",
"Reject invitation": "Reject invitation",
"Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?",
"Failed to reject invitation": "Failed to reject invitation",
"Cannot create rooms in this community": "Cannot create rooms in this community",
"You do not have permission to create rooms in this community.": "You do not have permission to create rooms in this community.",
"You are the only person here. If you leave, no one will be able to join in the future, including you.": "You are the only person here. If you leave, no one will be able to join in the future, including you.",
"This space is not public. You will not be able to rejoin without an invite.": "This space is not public. You will not be able to rejoin without an invite.",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
@ -3131,12 +2964,6 @@
"Logout": "Logout",
"%(creator)s created this DM.": "%(creator)s created this DM.",
"%(creator)s created and configured the room.": "%(creator)s created and configured the room.",
"Your Communities": "Your Communities",
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
"You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
"Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
"You're all caught up": "You're all caught up",
"You have no visible notifications.": "You have no visible notifications.",
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
@ -3161,7 +2988,6 @@
"Find a room…": "Find a room…",
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
"Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s",
"Filter": "Filter",
"Filter rooms and people": "Filter rooms and people",
"Clear filter": "Clear filter",
@ -3202,10 +3028,7 @@
"Search names and descriptions": "Search names and descriptions",
"Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you",
"To view this Space, hide communities in your <a>preferences</a>": "To view this Space, hide communities in your <a>preferences</a>",
"To join this Space, hide communities in your <a>preferences</a>": "To join this Space, hide communities in your <a>preferences</a>",
"To view %(spaceName)s, you need an invite": "To view %(spaceName)s, you need an invite",
"Created from <Community />": "Created from <Community />",
"Welcome to <name/>": "Welcome to <name/>",
"Random": "Random",
"Support": "Support",
@ -3349,7 +3172,6 @@
"Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.",
"Commands": "Commands",
"Command Autocomplete": "Command Autocomplete",
"Community Autocomplete": "Community Autocomplete",
"Emoji Autocomplete": "Emoji Autocomplete",
"Notify the whole room": "Notify the whole room",
"Room Notification": "Room Notification",

View file

@ -31,12 +31,13 @@ import dis from './dispatcher/dispatcher';
import { Action } from './dispatcher/actions';
import { ViewUserPayload } from './dispatcher/payloads/ViewUserPayload';
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
import { showGroupReplacedWithSpacesDialog } from "./group_helpers";
export enum Type {
URL = "url",
UserId = "userid",
RoomAlias = "roomalias",
GroupId = "groupid"
GroupId = "groupid",
}
// Linkify stuff doesn't type scanner/parser/utils properly :/
@ -115,6 +116,11 @@ function onUserClick(event: MouseEvent, userId: string) {
});
}
function onGroupClick(event: MouseEvent, groupId: string) {
event.preventDefault();
showGroupReplacedWithSpacesDialog(groupId);
}
function onAliasClick(event: MouseEvent, roomAlias: string) {
event.preventDefault();
dis.dispatch<ViewRoomPayload>({
@ -125,11 +131,6 @@ function onAliasClick(event: MouseEvent, roomAlias: string) {
});
}
function onGroupClick(event: MouseEvent, groupId: string) {
event.preventDefault();
dis.dispatch({ action: 'view_group', group_id: groupId });
}
const escapeRegExp = function(string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
@ -197,6 +198,7 @@ export const options = {
onAliasClick(e, alias);
},
};
case Type.GroupId:
return {
// @ts-ignore see https://linkify.js.org/docs/options.html

View file

@ -212,17 +212,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_communities_v2_prototypes": {
isFeature: true,
labsGroup: LabGroup.Spaces,
displayName: _td(
"Communities v2 prototypes. Requires compatible homeserver. " +
"Highly experimental - use with caution.",
),
supportedLevels: LEVELS_FEATURE,
default: false,
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
},
"feature_pinning": {
isFeature: true,
labsGroup: LabGroup.Messaging,
@ -248,14 +237,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
default: false,
controller: new CustomStatusController(),
},
"feature_custom_tags": {
isFeature: true,
labsGroup: LabGroup.Experimental,
displayName: _td("Group & filter rooms by custom tags (refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE,
default: false,
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
},
"feature_voice_rooms": {
isFeature: true,
labsGroup: LabGroup.Rooms,
@ -592,14 +573,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td('Mirror local video feed'),
default: false,
},
"TagPanel.enableTagPanel": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable Community Filter Panel'),
default: true,
invertedSettingName: 'TagPanel.disableTagPanel',
// We force the value to true because the invertedSettingName causes it to flip
controller: new UIFeatureController(UIFeature.Communities, true),
},
"theme": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: "light",
@ -896,7 +869,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
description: _td("All rooms you're in will appear in Home."),
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: false,
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null),
},
"Spaces.enabledMetaSpaces": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
@ -907,15 +879,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
"Spaces.showPeopleInSpace": {
supportedLevels: [SettingLevel.ROOM_ACCOUNT],
default: true,
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", null),
},
"showCommunitiesInsteadOfSpaces": {
displayName: _td("Display Communities instead of Spaces"),
description: _td("Temporarily show communities instead of Spaces for this session. " +
"Support for this will be removed in the near future. This will reload Element."),
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: false,
controller: new ReloadOnChangeController(),
},
"developerMode": {
displayName: _td("Developer mode"),
@ -993,17 +956,6 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
[UIFeature.Flair]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
// Disable Flair when Communities are disabled
controller: new UIFeatureController(UIFeature.Communities),
},
[UIFeature.Communities]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
controller: new IncompatibleController("showCommunitiesInsteadOfSpaces", false, false),
},
[UIFeature.AdvancedSettings]: {
supportedLevels: LEVELS_UI_FEATURE,
default: true,

View file

@ -28,8 +28,6 @@ export enum UIFeature {
ShareSocial = "UIFeature.shareSocial",
IdentityServer = "UIFeature.identityServer",
ThirdPartyID = "UIFeature.thirdPartyId",
Flair = "UIFeature.flair",
Communities = "UIFeature.communities",
AdvancedSettings = "UIFeature.advancedSettings",
RoomHistorySettings = "UIFeature.roomHistorySettings",
TimelineEnableRelativeDates = "UIFeature.timelineEnableRelativeDates",

View file

@ -1,192 +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 { Room } from "matrix-js-sdk/src/models/room";
import * as utils from "matrix-js-sdk/src/utils";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import { logger } from "matrix-js-sdk/src/logger";
import { Method } from "matrix-js-sdk/src/http-api";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { ActionPayload } from "../dispatcher/payloads";
import { Action } from "../dispatcher/actions";
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
import SettingsStore from "../settings/SettingsStore";
import { UPDATE_EVENT } from "./AsyncStore";
import FlairStore from "./FlairStore";
import GroupFilterOrderStore from "./GroupFilterOrderStore";
import GroupStore from "./GroupStore";
import dis from "../dispatcher/dispatcher";
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
interface IState {
// nothing of value - we use account data
}
export interface IRoomProfile {
displayName: string;
avatarMxc: string;
}
export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new CommunityPrototypeStore();
private constructor() {
super(defaultDispatcher, {});
}
public static get instance(): CommunityPrototypeStore {
return CommunityPrototypeStore.internalInstance;
}
public static getUpdateEventName(roomId: string): string {
return `${UPDATE_EVENT}:${roomId}`;
}
public getSelectedCommunityId(): string {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
return GroupFilterOrderStore.getSelectedTags()[0];
}
return null; // no selection as far as this function is concerned
}
public getSelectedCommunityName(): string {
return CommunityPrototypeStore.instance.getCommunityName(this.getSelectedCommunityId());
}
public getSelectedCommunityGeneralChat(): Room {
const communityId = this.getSelectedCommunityId();
if (communityId) {
return this.getGeneralChat(communityId);
}
}
public getCommunityName(communityId: string): string {
const profile = FlairStore.getGroupProfileCachedFast(this.matrixClient, communityId);
return profile?.name || communityId;
}
public getCommunityProfile(communityId: string): { name?: string, avatarUrl?: string } {
return FlairStore.getGroupProfileCachedFast(this.matrixClient, communityId);
}
public getGeneralChat(communityId: string): Room {
const rooms = GroupStore.getGroupRooms(communityId)
.map(r => this.matrixClient.getRoom(r.roomId))
.filter(r => !!r);
let chat = rooms.find(r => {
const idState = r.currentState.getStateEvents("im.vector.general_chat", "");
if (!idState || idState.getContent()['groupId'] !== communityId) return false;
return true;
});
if (!chat) chat = rooms[0];
return chat; // can be null
}
public isAdminOf(communityId: string): boolean {
const members = GroupStore.getGroupMembers(communityId);
const myMember = members.find(m => m.userId === this.matrixClient.getUserId());
return myMember?.isPrivileged;
}
public canInviteTo(communityId: string): boolean {
const generalChat = this.getGeneralChat(communityId);
if (!generalChat) return this.isAdminOf(communityId);
const myMember = generalChat.getMember(this.matrixClient.getUserId());
if (!myMember) return this.isAdminOf(communityId);
const pl = generalChat.currentState.getStateEvents("m.room.power_levels", "");
if (!pl) return this.isAdminOf(communityId);
const plContent = pl.getContent();
const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite);
return invitePl <= myMember.powerLevel;
}
protected async onAction(payload: ActionPayload): Promise<any> {
if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) {
return;
}
if (payload.action === "MatrixActions.Room.myMembership") {
const room: Room = payload.room;
const membership = getEffectiveMembership(payload.membership);
const oldMembership = getEffectiveMembership(payload.oldMembership);
if (membership === oldMembership) return;
if (membership === EffectiveMembership.Invite) {
try {
const path = utils.encodeUri("/rooms/$roomId/group_info", { $roomId: room.roomId });
const profile = await this.matrixClient.http.authedRequest(
undefined, Method.Get, path,
undefined, undefined,
{ prefix: "/_matrix/client/unstable/im.vector.custom" });
// we use global account data because per-room account data on invites is unreliable
await this.matrixClient.setAccountData("im.vector.group_info." + room.roomId, profile);
} catch (e) {
logger.warn("Non-fatal error getting group information for invite:", e);
}
}
} else if (payload.action === "MatrixActions.accountData") {
if (payload.event_type.startsWith("im.vector.group_info.")) {
const roomId = payload.event_type.substring("im.vector.group_info.".length);
this.emit(CommunityPrototypeStore.getUpdateEventName(roomId), roomId);
}
} else if (payload.action === "select_tag") {
// Automatically select the general chat when switching communities
const chat = this.getGeneralChat(payload.tag);
if (chat) {
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: chat.roomId,
metricsTrigger: undefined, // Deprecated groups
});
}
}
}
public getInviteProfile(roomId: string): IRoomProfile {
if (!this.matrixClient) return { displayName: null, avatarMxc: null };
const room = this.matrixClient.getRoom(roomId);
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId);
if (data && data.getContent()) {
return {
displayName: data.getContent().name,
avatarMxc: data.getContent().avatar_url,
};
}
}
return {
displayName: room.name,
avatarMxc: room.getMxcAvatarUrl(),
};
}
protected async onReady(): Promise<any> {
for (const room of this.matrixClient.getRooms()) {
const myMember = room.currentState.getMembers().find(m => m.userId === this.matrixClient.getUserId());
if (!myMember) continue;
if (getEffectiveMembership(myMember.membership) === EffectiveMembership.Invite) {
// Fake an update for anything that might have started listening before the invite
// data was available (eg: RoomPreviewBar after a refresh)
this.emit(CommunityPrototypeStore.getUpdateEventName(room.roomId), room.roomId);
}
}
}
}

View file

@ -1,157 +0,0 @@
/*
Copyright 2019 New Vector Ltd
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 EventEmitter from 'events';
import { throttle } from "lodash";
import dis from '../dispatcher/dispatcher';
import SettingsStore from "../settings/SettingsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "./room-list/RoomListStore";
import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore";
import { isCustomTag } from "./room-list/models";
import { objectHasDiff } from "../utils/objects";
function commonPrefix(a, b) {
const len = Math.min(a.length, b.length);
let prefix;
for (let i = 0; i < len; ++i) {
if (a.charAt(i) !== b.charAt(i)) {
prefix = a.substr(0, i);
break;
}
}
if (prefix === undefined) {
prefix = a.substr(0, len);
}
const spaceIdx = prefix.indexOf(' ');
if (spaceIdx !== -1) {
prefix = prefix.substr(0, spaceIdx + 1);
}
if (prefix.length >= 2) {
return prefix;
}
return "";
}
/**
* A class for storing application state for ordering tags in the GroupFilterPanel.
*/
class CustomRoomTagStore extends EventEmitter {
constructor() {
super();
// Initialise state
this._state = { tags: {} };
// as RoomListStore gets updated by every timeline event
// throttle this to only run every 500ms
this._getUpdatedTags = throttle(
this._getUpdatedTags, 500, {
leading: true,
trailing: true,
},
);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this._onListsUpdated);
dis.register(payload => this._onDispatch(payload));
}
getTags() {
return this._state.tags;
}
_setState(newState) {
this._state = Object.assign(this._state, newState);
this.emit("change");
}
addListener(callback) {
this.on("change", callback);
return {
remove: () => {
this.removeListener("change", callback);
},
};
}
getSortedTags() {
const tagNames = Object.keys(this._state.tags).sort();
const prefixes = tagNames.map((name, i) => {
const isFirst = i === 0;
const isLast = i === tagNames.length - 1;
const backwardsPrefix = !isFirst ? commonPrefix(name, tagNames[i - 1]) : "";
const forwardsPrefix = !isLast ? commonPrefix(name, tagNames[i + 1]) : "";
const longestPrefix = backwardsPrefix.length > forwardsPrefix.length ?
backwardsPrefix : forwardsPrefix;
return longestPrefix;
});
return tagNames.map((name, i) => {
const notifs = RoomNotificationStateStore.instance.getListState(name);
let badgeNotifState;
if (notifs.hasUnreadCount) {
badgeNotifState = notifs;
}
const avatarLetter = name.substr(prefixes[i].length, 1);
const selected = this._state.tags[name];
return { name, avatarLetter, badgeNotifState, selected };
});
}
_onListsUpdated = () => {
const newTags = this._getUpdatedTags();
if (!this._state.tags || objectHasDiff(this._state.tags, newTags)) {
this._setState({ tags: newTags });
}
};
_onDispatch(payload) {
switch (payload.action) {
case 'select_custom_room_tag': {
const oldTags = this._state.tags;
if (oldTags.hasOwnProperty(payload.tag)) {
const tag = {};
tag[payload.tag] = !oldTags[payload.tag];
const tags = Object.assign({}, oldTags, tag);
this._setState({ tags });
}
break;
}
case 'on_client_not_viable':
case 'on_logged_out': {
// we assume to always have a tags object in the state
this._state = { tags: {} };
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this._onListsUpdated);
break;
}
}
}
_getUpdatedTags() {
if (!SettingsStore.getValue("feature_custom_tags")) {
return {}; // none
}
const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort();
const prevTags = this._state && this._state.tags;
return newTagNames.reduce((c, tagName) => {
c[tagName] = (prevTags && prevTags[tagName]) || false;
return c;
}, {});
}
}
if (global.singletonCustomRoomTagStore === undefined) {
global.singletonCustomRoomTagStore = new CustomRoomTagStore();
}
export default global.singletonCustomRoomTagStore;

View file

@ -1,234 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
import { logger } from "matrix-js-sdk/src/logger";
const BULK_REQUEST_DEBOUNCE_MS = 200;
// Does the server support groups? Assume yes until we receive M_UNRECOGNIZED.
// If true, flair can function and we should keep sending requests for groups and avatars.
let groupSupport = true;
const USER_GROUPS_CACHE_BUST_MS = 1800000; // 30 mins
const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins
/**
* Stores data used by <Flair/>
*/
class FlairStore extends EventEmitter {
constructor(matrixClient) {
super();
this._matrixClient = matrixClient;
this._userGroups = {
// $userId: ['+group1:domain', '+group2:domain', ...]
};
this._groupProfiles = {
// $groupId: {
// avatar_url: 'mxc://...'
// }
};
this._groupProfilesPromise = {
// $groupId: Promise
};
this._usersPending = {
// $userId: {
// prom: Promise
// resolve: () => {}
// reject: () => {}
// }
};
this._usersInFlight = {
// This has the same schema as _usersPending
};
this._debounceTimeoutID = null;
}
groupSupport() {
return groupSupport;
}
invalidatePublicisedGroups(userId) {
delete this._userGroups[userId];
}
cachedPublicisedGroups(userId) {
return this._userGroups[userId];
}
getPublicisedGroupsCached(matrixClient, userId) {
if (this._userGroups[userId]) {
return Promise.resolve(this._userGroups[userId]);
}
// Bulk lookup ongoing, return promise to resolve/reject
if (this._usersPending[userId]) {
return this._usersPending[userId].prom;
}
// User has been moved from pending to in-flight
if (this._usersInFlight[userId]) {
return this._usersInFlight[userId].prom;
}
this._usersPending[userId] = {};
this._usersPending[userId].prom = new Promise((resolve, reject) => {
this._usersPending[userId].resolve = resolve;
this._usersPending[userId].reject = reject;
}).then((groups) => {
this._userGroups[userId] = groups;
setTimeout(() => {
delete this._userGroups[userId];
}, USER_GROUPS_CACHE_BUST_MS);
return this._userGroups[userId];
}).catch((err) => {
// Indicate whether the homeserver supports groups
if (err.errcode === 'M_UNRECOGNIZED') {
logger.warn('Cannot display flair, server does not support groups');
groupSupport = false;
// Return silently to avoid spamming for non-supporting servers
return;
}
logger.error('Could not get groups for user', userId, err);
throw err;
}).finally(() => {
delete this._usersInFlight[userId];
});
// This debounce will allow consecutive requests for the public groups of users that
// are sent in intervals of < BULK_REQUEST_DEBOUNCE_MS to be batched and only requested
// when no more requests are received within the next BULK_REQUEST_DEBOUNCE_MS. The naive
// implementation would do a request that only requested the groups for `userId`, leading
// to a worst and best case of 1 user per request. This implementation's worst is still
// 1 user per request but only if the requests are > BULK_REQUEST_DEBOUNCE_MS apart and the
// best case is N users per request.
//
// This is to reduce the number of requests made whilst trading off latency when viewing
// a Flair component.
if (this._debounceTimeoutID) clearTimeout(this._debounceTimeoutID);
this._debounceTimeoutID = setTimeout(() => {
this._batchedGetPublicGroups(matrixClient);
}, BULK_REQUEST_DEBOUNCE_MS);
return this._usersPending[userId].prom;
}
async _batchedGetPublicGroups(matrixClient) {
// Move users pending to users in flight
this._usersInFlight = this._usersPending;
this._usersPending = {};
let resp = {
users: [],
};
try {
resp = await matrixClient.getPublicisedGroups(Object.keys(this._usersInFlight));
} catch (err) {
// Propagate the same error to all usersInFlight
Object.keys(this._usersInFlight).forEach((userId) => {
// The promise should always exist for userId, but do a null-check anyway
if (!this._usersInFlight[userId]) return;
this._usersInFlight[userId].reject(err);
});
return;
}
const updatedUserGroups = resp.users;
Object.keys(this._usersInFlight).forEach((userId) => {
// The promise should always exist for userId, but do a null-check anyway
if (!this._usersInFlight[userId]) return;
this._usersInFlight[userId].resolve(updatedUserGroups[userId] || []);
});
}
/**
* Gets the profile for the given group if known, otherwise returns null.
* This triggers `getGroupProfileCached` if needed, though the result of the
* call will not be returned by this function.
* @param {MatrixClient} matrixClient The matrix client to use to fetch the profile, if needed.
* @param {string} groupId The group ID to get the profile for.
* @returns {*} The profile if known, otherwise null.
*/
getGroupProfileCachedFast(matrixClient, groupId) {
if (!matrixClient || !groupId) return null;
if (this._groupProfiles[groupId]) {
return this._groupProfiles[groupId];
}
this.getGroupProfileCached(matrixClient, groupId);
return null;
}
async getGroupProfileCached(matrixClient, groupId) {
if (this._groupProfiles[groupId]) {
return this._groupProfiles[groupId];
}
// A request is ongoing, wait for it to complete and return the group profile.
if (this._groupProfilesPromise[groupId]) {
try {
await this._groupProfilesPromise[groupId];
} catch (e) {
// Don't log the error; this is done below
return null;
}
return this._groupProfiles[groupId];
}
// No request yet, start one
logger.log('FlairStore: Request group profile of ' + groupId);
this._groupProfilesPromise[groupId] = matrixClient.getGroupProfile(groupId);
let profile;
try {
profile = await this._groupProfilesPromise[groupId];
} catch (e) {
logger.log('FlairStore: Failed to get group profile for ' + groupId, e);
// Don't retry, but allow a retry when the profile is next requested
delete this._groupProfilesPromise[groupId];
return null;
}
this._groupProfiles[groupId] = {
groupId,
avatarUrl: profile.avatar_url,
name: profile.name,
shortDescription: profile.short_description,
};
delete this._groupProfilesPromise[groupId];
/// XXX: This is verging on recreating a third "Flux"-looking Store. We really
/// should replace FlairStore with a Flux store and some async actions.
logger.log('FlairStore: Emit updateGroupProfile for ' + groupId);
this.emit('updateGroupProfile');
setTimeout(() => {
this.refreshGroupProfile(matrixClient, groupId);
}, GROUP_PROFILES_CACHE_BUST_MS);
return this._groupProfiles[groupId];
}
refreshGroupProfile(matrixClient, groupId) {
// Invalidate the cache
delete this._groupProfiles[groupId];
// Fetch new profile data, and cache it
return this.getGroupProfileCached(matrixClient, groupId);
}
}
if (global.singletonFlairStore === undefined) {
global.singletonFlairStore = new FlairStore();
}
export default global.singletonFlairStore;

View file

@ -1,281 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Store } from 'flux/utils';
import { EventType } from "matrix-js-sdk/src/@types/event";
import dis from '../dispatcher/dispatcher';
import { Action } from '../dispatcher/actions';
import GroupStore from './GroupStore';
import Analytics from '../Analytics';
import * as RoomNotifs from "../RoomNotifs";
import { MatrixClientPeg } from '../MatrixClientPeg';
import SettingsStore from "../settings/SettingsStore";
import { CreateEventField } from "../@types/groups";
const INITIAL_STATE = {
orderedTags: null,
orderedTagsAccountData: null,
hasSynced: false,
joinedGroupIds: null,
selectedTags: [],
// Last selected tag when shift was not being pressed
anchorTag: null,
};
/**
* A class for storing application state for ordering tags in the GroupFilterPanel.
*/
class GroupFilterOrderStore extends Store {
constructor() {
super(dis);
// Initialise state
this._state = Object.assign({}, INITIAL_STATE);
SettingsStore.monitorSetting("TagPanel.enableTagPanel", null);
}
_setState(newState) {
this._state = Object.assign(this._state, newState);
this.__emitChange();
}
__onDispatch(payload) { // eslint-disable-line @typescript-eslint/naming-convention
switch (payload.action) {
// Initialise state after initial sync
case Action.ViewRoom: {
const relatedGroupIds = GroupStore.getGroupIdsForRoomId(payload.room_id);
this._updateBadges(relatedGroupIds);
break;
}
case 'MatrixActions.sync': {
if (payload.state === 'SYNCING' || payload.state === 'PREPARED') {
this._updateBadges();
}
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
break;
}
const tagOrderingEvent = payload.matrixClient.getAccountData('im.vector.web.tag_ordering');
const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {};
this._setState({
orderedTagsAccountData: tagOrderingEventContent.tags || null,
removedTagsAccountData: tagOrderingEventContent.removedTags || null,
hasSynced: true,
});
this._updateOrderedTags();
break;
}
// Get ordering from account data
case 'MatrixActions.accountData': {
if (payload.event_type !== 'im.vector.web.tag_ordering') break;
// Ignore remote echos caused by this store so as to avoid setting
// state back to old state.
if (payload.event_content._storeId === this.getStoreId()) break;
this._setState({
orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
removedTagsAccountData: payload.event_content ? payload.event_content.removedTags : null,
});
this._updateOrderedTags();
break;
}
// Initialise the state such that if account data is unset, default to joined groups
case 'GroupActions.fetchJoinedGroups.success': {
this._setState({
joinedGroupIds: payload.result.groups.sort(), // Sort lexically
hasFetchedJoinedGroups: true,
});
this._updateOrderedTags();
break;
}
case 'TagOrderActions.moveTag.pending': {
// Optimistic update of a moved tag
this._setState({
orderedTags: payload.request.tags,
removedTagsAccountData: payload.request.removedTags,
});
break;
}
case 'TagOrderActions.removeTag.pending': {
// Optimistic update of a removed tag
this._setState({
removedTagsAccountData: payload.request.removedTags,
});
this._updateOrderedTags();
break;
}
case 'select_tag': {
const allowMultiple = !SettingsStore.getValue("feature_communities_v2_prototypes");
let newTags = [];
// Shift-click semantics
if (payload.shiftKey && allowMultiple) {
// Select range of tags
let start = this._state.orderedTags.indexOf(this._state.anchorTag);
let end = this._state.orderedTags.indexOf(payload.tag);
if (start === -1) {
start = end;
}
if (start > end) {
const temp = start;
start = end;
end = temp;
}
newTags = payload.ctrlOrCmdKey ? this._state.selectedTags : [];
newTags = [...new Set(
this._state.orderedTags.slice(start, end + 1).concat(newTags),
)];
} else {
if (payload.ctrlOrCmdKey && allowMultiple) {
// Toggle individual tag
if (this._state.selectedTags.includes(payload.tag)) {
newTags = this._state.selectedTags.filter((t) => t !== payload.tag);
} else {
newTags = [...this._state.selectedTags, payload.tag];
}
} else {
if (this._state.selectedTags.length === 1 && this._state.selectedTags.includes(payload.tag)) {
// Existing (only) selected tag is being normally clicked again, clear tags
newTags = [];
} else {
// Select individual tag
newTags = [payload.tag];
}
}
// Only set the anchor tag if the tag was previously unselected, otherwise
// the next range starts with an unselected tag.
if (!this._state.selectedTags.includes(payload.tag)) {
this._setState({
anchorTag: payload.tag,
});
}
}
this._setState({
selectedTags: newTags,
});
Analytics.trackEvent('FilterStore', 'select_tag');
}
break;
case 'deselect_tags':
if (payload.tag) {
// if a tag is passed, only deselect that tag
this._setState({
selectedTags: this._state.selectedTags.filter(tag => tag !== payload.tag),
});
} else {
this._setState({
selectedTags: [],
});
}
Analytics.trackEvent('FilterStore', 'deselect_tags');
break;
case 'on_client_not_viable':
case 'on_logged_out': {
// Reset state without pushing an update to the view, which generally assumes that
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
this._state = Object.assign({}, INITIAL_STATE);
break;
}
case 'setting_updated':
if (payload.settingName === 'TagPanel.enableTagPanel' && !payload.newValue) {
this._setState({
selectedTags: [],
});
Analytics.trackEvent('FilterStore', 'disable_tags');
}
break;
}
}
_updateBadges(groupIds = this._state.joinedGroupIds) {
if (groupIds && groupIds.length) {
const client = MatrixClientPeg.get();
const changedBadges = {};
groupIds.forEach(groupId => {
const rooms =
GroupStore.getGroupRooms(groupId)
.map(r => client.getRoom(r.roomId)) // to Room objects
.filter(r => r !== null && r !== undefined); // filter out rooms we haven't joined from the group
const badge = rooms && RoomNotifs.aggregateNotificationCount(rooms);
changedBadges[groupId] = (badge && badge.count !== 0) ? badge : undefined;
});
const newBadges = Object.assign({}, this._state.badges, changedBadges);
this._setState({ badges: newBadges });
}
}
_updateOrderedTags() {
this._setState({
orderedTags:
this._state.hasSynced &&
this._state.hasFetchedJoinedGroups ?
this._mergeGroupsAndTags() : null,
});
}
_mergeGroupsAndTags() {
const groupIds = this._state.joinedGroupIds || [];
const tags = this._state.orderedTagsAccountData || [];
const removedTags = new Set(this._state.removedTagsAccountData || []);
const tagsToKeep = tags.filter(
(t) => (t[0] !== '+' || groupIds.includes(t)) && !removedTags.has(t),
);
const cli = MatrixClientPeg.get();
const migratedCommunities = new Set(cli.getRooms().map(r => {
return r.currentState.getStateEvents(EventType.RoomCreate, "")?.getContent()[CreateEventField];
}).filter(Boolean));
const groupIdsToAdd = groupIds.filter(
(groupId) => !tags.includes(groupId) && !removedTags.has(groupId) && !migratedCommunities.has(groupId),
);
return tagsToKeep.concat(groupIdsToAdd);
}
getGroupBadge(groupId) {
const badges = this._state.badges;
return badges && badges[groupId];
}
getOrderedTags() {
return this._state.orderedTags;
}
getRemovedTagsAccountData() {
return this._state.removedTagsAccountData;
}
getStoreId() {
// Generate a random ID to prevent this store from clobbering its
// state with redundant remote echos.
if (!this._id) this._id = Math.random().toString(16).slice(2, 10);
return this._id;
}
getSelectedTags() {
return this._state.selectedTags;
}
}
if (global.singletonGroupFilterOrderStore === undefined) {
global.singletonGroupFilterOrderStore = new GroupFilterOrderStore();
}
export default global.singletonGroupFilterOrderStore;

View file

@ -1,351 +0,0 @@
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import EventEmitter from 'events';
import { logger } from "matrix-js-sdk/src/logger";
import { groupMemberFromApiObject, groupRoomFromApiObject } from '../groups';
import FlairStore from './FlairStore';
import { MatrixClientPeg } from '../MatrixClientPeg';
import dis from '../dispatcher/dispatcher';
export function parseMembersResponse(response) {
return response.chunk.map((apiMember) => groupMemberFromApiObject(apiMember));
}
export function parseRoomsResponse(response) {
return response.chunk.map((apiRoom) => groupRoomFromApiObject(apiRoom));
}
// The number of ongoing group requests
let ongoingRequestCount = 0;
// This has arbitrarily been set to a small number to lower the priority
// of doing group-related requests because we care about other important
// requests like hitting /sync.
const LIMIT = 3; // Maximum number of ongoing group requests
// FIFO queue of functions to call in the backlog
const backlogQueue = [
// () => {...}
];
// Pull from the FIFO queue
function checkBacklog() {
const item = backlogQueue.shift();
if (typeof item === 'function') item();
}
// Limit the maximum number of ongoing promises returned by fn to LIMIT and
// use a FIFO queue to handle the backlog.
async function limitConcurrency(fn) {
if (ongoingRequestCount >= LIMIT) {
// Enqueue this request for later execution
await new Promise((resolve, reject) => {
backlogQueue.push(resolve);
});
}
ongoingRequestCount++;
try {
return await fn();
} catch (err) {
// We explicitly do not handle the error here, but let it propogate.
throw err;
} finally {
ongoingRequestCount--;
checkBacklog();
}
}
/**
* Global store for tracking group summary, members, invited members and rooms.
*/
class GroupStore extends EventEmitter {
STATE_KEY = {
GroupMembers: 'GroupMembers',
GroupInvitedMembers: 'GroupInvitedMembers',
Summary: 'Summary',
GroupRooms: 'GroupRooms',
};
constructor() {
super();
this._state = {};
this._state[this.STATE_KEY.Summary] = {};
this._state[this.STATE_KEY.GroupRooms] = {};
this._state[this.STATE_KEY.GroupMembers] = {};
this._state[this.STATE_KEY.GroupInvitedMembers] = {};
this._ready = {};
this._ready[this.STATE_KEY.Summary] = {};
this._ready[this.STATE_KEY.GroupRooms] = {};
this._ready[this.STATE_KEY.GroupMembers] = {};
this._ready[this.STATE_KEY.GroupInvitedMembers] = {};
this._fetchResourcePromise = {
[this.STATE_KEY.Summary]: {},
[this.STATE_KEY.GroupRooms]: {},
[this.STATE_KEY.GroupMembers]: {},
[this.STATE_KEY.GroupInvitedMembers]: {},
};
this._resourceFetcher = {
[this.STATE_KEY.Summary]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupSummary(groupId),
);
},
[this.STATE_KEY.GroupRooms]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupRooms(groupId).then(parseRoomsResponse),
);
},
[this.STATE_KEY.GroupMembers]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupUsers(groupId).then(parseMembersResponse),
);
},
[this.STATE_KEY.GroupInvitedMembers]: (groupId) => {
return limitConcurrency(
() => MatrixClientPeg.get().getGroupInvitedUsers(groupId).then(parseMembersResponse),
);
},
};
}
_fetchResource(stateKey, groupId) {
// Ongoing request, ignore
if (this._fetchResourcePromise[stateKey][groupId]) return;
const clientPromise = this._resourceFetcher[stateKey](groupId);
// Indicate ongoing request
this._fetchResourcePromise[stateKey][groupId] = clientPromise;
clientPromise.then((result) => {
this._state[stateKey][groupId] = result;
this._ready[stateKey][groupId] = true;
this._notifyListeners();
}).catch((err) => {
// Invited users not visible to non-members
if (stateKey === this.STATE_KEY.GroupInvitedMembers && err.httpStatus === 403) {
return;
}
logger.error(`Failed to get resource ${stateKey} for ${groupId}`, err);
this.emit('error', err, groupId, stateKey);
}).finally(() => {
// Indicate finished request, allow for future fetches
delete this._fetchResourcePromise[stateKey][groupId];
});
return clientPromise;
}
_notifyListeners() {
this.emit('update');
}
/**
* Register a listener to recieve updates from the store. This also
* immediately triggers an update to send the current state of the
* store (which could be the initial state).
*
* If a group ID is specified, this also causes a fetch of all data
* of the specified group, which might cause 4 separate HTTP
* requests, but only if said requests aren't already ongoing.
*
* @param {string?} groupId the ID of the group to fetch data for.
* Optional.
* @param {function} fn the function to call when the store updates.
* @return {Object} tok a registration "token" with a single
* property `unregister`, a function that can
* be called to unregister the listener such
* that it won't be called any more.
*/
registerListener(groupId, fn) {
this.on('update', fn);
// Call to set initial state (before fetching starts)
this.emit('update');
if (groupId) {
this._fetchResource(this.STATE_KEY.Summary, groupId);
this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
this._fetchResource(this.STATE_KEY.GroupInvitedMembers, groupId);
}
// Similar to the Store of flux/utils, we return a "token" that
// can be used to unregister the listener.
return {
unregister: () => {
this.unregisterListener(fn);
},
};
}
unregisterListener(fn) {
this.removeListener('update', fn);
}
isStateReady(groupId, id) {
return this._ready[id][groupId];
}
getGroupIdsForRoomId(roomId) {
const groupIds = Object.keys(this._state[this.STATE_KEY.GroupRooms]);
return groupIds.filter(groupId => {
const rooms = this._state[this.STATE_KEY.GroupRooms][groupId] || [];
return rooms.some(room => room.roomId === roomId);
});
}
getSummary(groupId) {
return this._state[this.STATE_KEY.Summary][groupId] || {};
}
getGroupRooms(groupId) {
return this._state[this.STATE_KEY.GroupRooms][groupId] || [];
}
getGroupMembers(groupId) {
return this._state[this.STATE_KEY.GroupMembers][groupId] || [];
}
getGroupInvitedMembers(groupId) {
return this._state[this.STATE_KEY.GroupInvitedMembers][groupId] || [];
}
getGroupPublicity(groupId) {
return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
(this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_publicised : null;
}
isUserPrivileged(groupId) {
return (this._state[this.STATE_KEY.Summary][groupId] || {}).user ?
(this._state[this.STATE_KEY.Summary][groupId] || {}).user.is_privileged : null;
}
refreshGroupRooms(groupId) {
return this._fetchResource(this.STATE_KEY.GroupRooms, groupId);
}
refreshGroupMembers(groupId) {
return this._fetchResource(this.STATE_KEY.GroupMembers, groupId);
}
addRoomToGroup(groupId, roomId, isPublic) {
return MatrixClientPeg.get()
.addRoomToGroup(groupId, roomId, isPublic)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
updateGroupRoomVisibility(groupId, roomId, isPublic) {
return MatrixClientPeg.get()
.updateGroupRoomVisibility(groupId, roomId, isPublic)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
removeRoomFromGroup(groupId, roomId) {
return MatrixClientPeg.get()
.removeRoomFromGroup(groupId, roomId)
// Room might be in the summary, refresh just in case
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId));
}
inviteUserToGroup(groupId, userId) {
return MatrixClientPeg.get().inviteUserToGroup(groupId, userId)
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
acceptGroupInvite(groupId) {
return MatrixClientPeg.get().acceptGroupInvite(groupId)
// The user should now be able to access (personal) group settings
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might be able to see more rooms now
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now appear as a member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
// The user should now not appear as an invited member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
joinGroup(groupId) {
return MatrixClientPeg.get().joinGroup(groupId)
// The user should now be able to access (personal) group settings
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might be able to see more rooms now
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now appear as a member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId))
// The user should now not appear as an invited member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupInvitedMembers, groupId));
}
leaveGroup(groupId) {
// ensure the tag panel filter is cleared if the group was selected
dis.dispatch({
action: "deselect_tags",
tag: groupId,
});
return MatrixClientPeg.get().leaveGroup(groupId)
// The user should now not be able to access group settings
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId))
// The user might only be able to see a subset of rooms now
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupRooms, groupId))
// The user should now not appear as a member
.then(this._fetchResource.bind(this, this.STATE_KEY.GroupMembers, groupId));
}
addRoomToGroupSummary(groupId, roomId, categoryId) {
return MatrixClientPeg.get()
.addRoomToGroupSummary(groupId, roomId, categoryId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
addUserToGroupSummary(groupId, userId, roleId) {
return MatrixClientPeg.get()
.addUserToGroupSummary(groupId, userId, roleId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
removeRoomFromGroupSummary(groupId, roomId) {
return MatrixClientPeg.get()
.removeRoomFromGroupSummary(groupId, roomId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
removeUserFromGroupSummary(groupId, userId) {
return MatrixClientPeg.get()
.removeUserFromGroupSummary(groupId, userId)
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
setGroupPublicity(groupId, isPublished) {
return MatrixClientPeg.get()
.setGroupPublicity(groupId, isPublished)
.then(() => { FlairStore.invalidatePublicisedGroups(MatrixClientPeg.get().credentials.userId); })
.then(this._fetchResource.bind(this, this.STATE_KEY.Summary, groupId));
}
}
let singletonGroupStore = null;
if (!singletonGroupStore) {
singletonGroupStore = new GroupStore();
}
export default singletonGroupStore;

View file

@ -123,11 +123,8 @@ class RoomViewStore extends Store<ActionPayload> {
this.viewRoom(payload);
break;
// for these events blank out the roomId as we are no longer in the RoomView
case 'view_create_group':
case 'view_welcome_page':
case Action.ViewHomePage:
case 'view_my_groups':
case 'view_group':
this.setState({
roomId: null,
roomAlias: null,

Some files were not shown because too many files have changed in this diff Show more