Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/dpsah/6785.2
This commit is contained in:
commit
368571bcff
31 changed files with 747 additions and 119 deletions
|
@ -20,7 +20,7 @@ import createReactClass from 'create-react-class';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Key } from '../../Keyboard';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { throttle } from 'lodash';
|
||||
import {throttle} from 'lodash';
|
||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
|
|
@ -42,6 +42,14 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../views/context_menus/IconizedContextMenu";
|
||||
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import TagOrderStore from "../../stores/TagOrderStore";
|
||||
import { showCommunityInviteDialog } from "../../RoomInvite";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||
import EditCommunityPrototypeDialog from "../views/dialogs/EditCommunityPrototypeDialog";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -58,6 +66,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
private dispatcherRef: string;
|
||||
private themeWatcherRef: string;
|
||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||
private tagStoreRef: fbEmitter.EventSubscription;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -77,14 +86,20 @@ 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 = TagOrderStore.addListener(this.onTagStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
||||
this.tagStoreRef.remove();
|
||||
}
|
||||
|
||||
private onTagStoreUpdate = () => {
|
||||
this.forceUpdate(); // we don't have anything useful in state to update
|
||||
};
|
||||
|
||||
private isUserOnDarkTheme(): boolean {
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme.startsWith("custom-")) {
|
||||
|
@ -189,9 +204,54 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
||||
};
|
||||
|
||||
private onCommunitySettingsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
|
||||
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
|
||||
});
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private 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({
|
||||
action: 'view_room',
|
||||
room_id: chat.roomId,
|
||||
}, true);
|
||||
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
|
||||
} 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"),
|
||||
});
|
||||
}
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onCommunityInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private renderContextMenu = (): React.ReactNode => {
|
||||
if (!this.state.contextMenuPosition) return null;
|
||||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let hostingLink;
|
||||
const signupLink = getHostingLink("user-context-menu");
|
||||
if (signupLink) {
|
||||
|
@ -225,22 +285,137 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
return <IconizedContextMenu
|
||||
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
|
||||
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
|
||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
||||
onFinished={this.onCloseMenu}
|
||||
className="mx_UserMenu_contextMenu"
|
||||
>
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
let primaryHeader = (
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{OwnProfileStore.instance.displayName}
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{MatrixClientPeg.get().getUserId()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
let primaryOptionList = (
|
||||
<React.Fragment>
|
||||
<IconizedContextMenuOptionList>
|
||||
{homeButton}
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("Notification settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & privacy")}
|
||||
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("All settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{/* <IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconArchive"
|
||||
label={_t("Archived rooms")}
|
||||
onClick={this.onShowArchived}
|
||||
/> */}
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMessage"
|
||||
label={_t("Feedback")}
|
||||
onClick={this.onProvideFeedback}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
);
|
||||
let secondarySection = null;
|
||||
|
||||
if (prototypeCommunityName) {
|
||||
primaryHeader = (
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{OwnProfileStore.instance.displayName}
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{MatrixClientPeg.get().getUserId()}
|
||||
{prototypeCommunityName}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
primaryOptionList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("Community settings")}
|
||||
onClick={this.onCommunitySettingsClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={this.onCommunityMembersClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconInvite"
|
||||
label={_t("Invite")}
|
||||
onClick={this.onCommunityInviteClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
secondarySection = (
|
||||
<React.Fragment>
|
||||
<hr />
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
<div className="mx_UserMenu_contextMenu_name">
|
||||
<span className="mx_UserMenu_contextMenu_displayName">
|
||||
{OwnProfileStore.instance.displayName}
|
||||
</span>
|
||||
<span className="mx_UserMenu_contextMenu_userId">
|
||||
{MatrixClientPeg.get().getUserId()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("Settings")}
|
||||
aria-label={_t("User settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMessage"
|
||||
label={_t("Feedback")}
|
||||
onClick={this.onProvideFeedback}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_UserMenu_contextMenu": true,
|
||||
"mx_UserMenu_contextMenu_prototype": !!prototypeCommunityName,
|
||||
});
|
||||
|
||||
return <IconizedContextMenu
|
||||
// numerical adjustments to overlap the context menu by just over the width of the
|
||||
// menu icon and make it look connected
|
||||
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 10}
|
||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height + 8}
|
||||
onFinished={this.onCloseMenu}
|
||||
className={classes}
|
||||
>
|
||||
<div className="mx_UserMenu_contextMenu_header">
|
||||
{primaryHeader}
|
||||
<AccessibleTooltipButton
|
||||
className="mx_UserMenu_contextMenu_themeButton"
|
||||
onClick={this.onSwitchThemeClick}
|
||||
|
@ -254,41 +429,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
{hostingLink}
|
||||
<IconizedContextMenuOptionList>
|
||||
{homeButton}
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconBell"
|
||||
label={_t("Notification settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconLock"
|
||||
label={_t("Security & privacy")}
|
||||
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSettings"
|
||||
label={_t("All settings")}
|
||||
onClick={(e) => this.onSettingsOpen(e, null)}
|
||||
/>
|
||||
{/* <IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconArchive"
|
||||
label={_t("Archived rooms")}
|
||||
onClick={this.onShowArchived}
|
||||
/> */}
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconMessage"
|
||||
label={_t("Feedback")}
|
||||
onClick={this.onProvideFeedback}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOptionList red>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_UserMenu_iconSignOut"
|
||||
label={_t("Sign out")}
|
||||
onClick={this.onSignOutClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
{primaryOptionList}
|
||||
{secondarySection}
|
||||
</IconizedContextMenu>;
|
||||
};
|
||||
|
||||
|
@ -298,12 +440,34 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
|
||||
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
||||
|
||||
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
||||
let isPrototype = false;
|
||||
let menuName = _t("User menu");
|
||||
let name = <span className="mx_UserMenu_userName">{displayName}</span>;
|
||||
let buttons = (
|
||||
<span className="mx_UserMenu_headerButtons">
|
||||
{/* masked image in CSS */}
|
||||
</span>
|
||||
);
|
||||
if (prototypeCommunityName) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{prototypeCommunityName}</span>
|
||||
<span className="mx_UserMenu_subUserName">{displayName}</span>
|
||||
</div>
|
||||
);
|
||||
menuName = _t("Community and user menu");
|
||||
isPrototype = true;
|
||||
} else if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
name = (
|
||||
<div className="mx_UserMenu_doubleName">
|
||||
<span className="mx_UserMenu_userName">{_t("Home")}</span>
|
||||
<span className="mx_UserMenu_subUserName">{displayName}</span>
|
||||
</div>
|
||||
);
|
||||
isPrototype = true;
|
||||
}
|
||||
if (this.props.isMinimized) {
|
||||
name = null;
|
||||
buttons = null;
|
||||
|
@ -312,6 +476,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
const classes = classNames({
|
||||
'mx_UserMenu': true,
|
||||
'mx_UserMenu_minimized': this.props.isMinimized,
|
||||
'mx_UserMenu_prototype': isPrototype,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -320,7 +485,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
className={classes}
|
||||
onClick={this.onOpenMenuClick}
|
||||
inputRef={this.buttonRef}
|
||||
label={_t("User menu")}
|
||||
label={menuName}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
|
|
|
@ -25,8 +25,7 @@ import { _t } from '../../../languageHandler';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import TagOrderStore from "../../../stores/TagOrderStore";
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
|
@ -72,8 +71,8 @@ export default createReactClass({
|
|||
opts.encryption = this.state.isEncrypted;
|
||||
}
|
||||
|
||||
if (TagOrderStore.getSelectedPrototypeTag()) {
|
||||
opts.associatedWithCommunity = TagOrderStore.getSelectedPrototypeTag();
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
}
|
||||
|
||||
return opts;
|
||||
|
@ -198,7 +197,7 @@ export default createReactClass({
|
|||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
||||
"found and joined by anyone.",
|
||||
)}</p>;
|
||||
if (TagOrderStore.getSelectedPrototypeTag()) {
|
||||
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.",
|
||||
|
@ -239,9 +238,8 @@ export default createReactClass({
|
|||
}
|
||||
|
||||
let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
||||
if (TagOrderStore.getSelectedPrototypeTag()) {
|
||||
const summary = GroupStore.getSummary(TagOrderStore.getSelectedPrototypeTag());
|
||||
const name = summary?.profile?.name || TagOrderStore.getSelectedPrototypeTag();
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
title = _t("Create a room in %(communityName)s", {communityName: name});
|
||||
}
|
||||
return (
|
||||
|
|
167
src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
Normal file
167
src/components/views/dialogs/EditCommunityPrototypeDialog.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
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 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";
|
||||
|
||||
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
|
||||
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) {
|
||||
console.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 = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl);
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -32,11 +32,12 @@ import IdentityAuthClient from "../../../IdentityAuthClient";
|
|||
import Modal from "../../../Modal";
|
||||
import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -909,12 +910,23 @@ export default class InviteDialog extends React.PureComponent {
|
|||
this.props.onFinished();
|
||||
};
|
||||
|
||||
_onCommunityInviteClick = (e) => {
|
||||
this.props.onFinished();
|
||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||
};
|
||||
|
||||
_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");
|
||||
|
@ -993,6 +1005,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
{sectionSubname ? <p className="mx_InviteDialog_subname">{sectionSubname}</p> : null}
|
||||
{tiles}
|
||||
{showMore}
|
||||
</div>
|
||||
|
@ -1083,6 +1096,33 @@ export default class InviteDialog extends React.PureComponent {
|
|||
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
|
||||
}},
|
||||
);
|
||||
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||
const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
helpText = _t(
|
||||
"Start a conversation with someone using their name, username (like <userId/>) or email address. " +
|
||||
"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>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else { // KIND_INVITE
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
import {debounce} from "lodash";
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import PropTypes from "prop-types";
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as sdk from '../../../index';
|
||||
import { debounce } from 'lodash';
|
||||
import {debounce} from "lodash";
|
||||
import {IFieldState, IValidationResult} from "./Validation";
|
||||
|
||||
// Invoke validation from user input (when typing, etc.) at most once every N ms.
|
||||
|
|
|
@ -331,8 +331,14 @@ export default class ReplyThread extends React.Component {
|
|||
{
|
||||
_t('<a>In reply to</a> <pill>', {}, {
|
||||
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyThread_show">{ sub }</a>,
|
||||
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
|
||||
url={makeUserPermalink(ev.getSender())} shouldShowPillAvatar={true} />,
|
||||
'pill': (
|
||||
<Pill
|
||||
type={Pill.TYPE_USER_MENTION}
|
||||
room={room}
|
||||
url={makeUserPermalink(ev.getSender())}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
</blockquote>;
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React, {createRef, KeyboardEvent} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import flatMap from 'lodash/flatMap';
|
||||
import {flatMap} from "lodash";
|
||||
import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter';
|
||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import rate_limited_func from "../../../ratelimitedfunc";
|
|||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from "../../../index";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -464,10 +465,16 @@ export default createReactClass({
|
|||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
inviteButton =
|
||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!canInvite}>
|
||||
<span>{ _t('Invite to this room') }</span>
|
||||
<span>{ inviteButtonText }</span>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
|
|||
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
||||
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import TagOrderStore from "../../../stores/TagOrderStore";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -130,7 +130,7 @@ const TAG_AESTHETICS: {
|
|||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={TagOrderStore.getSelectedPrototypeTag()
|
||||
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
|
||||
? _t("Explore community rooms")
|
||||
: _t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
|
|
|
@ -24,6 +24,7 @@ import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
|||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
@replaceableComponent("views.settings.BridgeTile")
|
||||
export default class BridgeTile extends React.PureComponent {
|
||||
|
@ -56,7 +57,7 @@ export default class BridgeTile extends React.PureComponent {
|
|||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(content.creator)}
|
||||
shouldShowPillAvatar={true}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
}
|
||||
|
@ -66,7 +67,7 @@ export default class BridgeTile extends React.PureComponent {
|
|||
type={Pill.TYPE_USER_MENTION}
|
||||
room={this.props.room}
|
||||
url={makeUserPermalink(this.props.ev.getSender())}
|
||||
shouldShowPillAvatar={true}
|
||||
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||
/>,
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue