mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 21:24:59 +08:00
Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/dpsah/6785.2
This commit is contained in:
commit
368571bcff
@ -68,6 +68,7 @@
|
||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
@import "./views/dialogs/_InviteDialog.scss";
|
||||
|
@ -16,9 +16,33 @@ limitations under the License.
|
||||
|
||||
.mx_UserMenu {
|
||||
|
||||
// to make the ... button sort of aligned with the explore button below
|
||||
// to make the menu button sort of aligned with the explore button below
|
||||
padding-right: 6px;
|
||||
|
||||
&.mx_UserMenu_prototype {
|
||||
// The margin & padding combination between here and the ::after is to
|
||||
// align the border line with the tag panel.
|
||||
margin-bottom: 6px;
|
||||
|
||||
padding-right: 0; // make the right edge line up with the explore button
|
||||
|
||||
.mx_UserMenu_headerButtons {
|
||||
// considering we've eliminated right padding on the menu itself, we need to
|
||||
// push the chevron in slightly (roughly lining up with the center of the
|
||||
// plus buttons)
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
// we cheat opacity on the theme colour with an after selector here
|
||||
&::after {
|
||||
content: '';
|
||||
border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse
|
||||
opacity: 0.2;
|
||||
display: block;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_headerButtons {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@ -36,7 +60,7 @@ limitations under the License.
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $primary-fg-color;
|
||||
mask-image: url('$(res)/img/element-icons/context-menu.svg');
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +80,28 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_doubleName {
|
||||
flex: 1;
|
||||
min-width: 0; // make flexbox aware that it can crush this to a tiny width
|
||||
|
||||
.mx_UserMenu_userName,
|
||||
.mx_UserMenu_subUserName {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_UserMenu_subUserName {
|
||||
color: $muted-fg-color;
|
||||
font-size: $font-13px;
|
||||
line-height: $font-18px;
|
||||
flex: 1;
|
||||
|
||||
// Ellipsize any text overflow
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_userName {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
@ -89,6 +135,44 @@ limitations under the License.
|
||||
.mx_UserMenu_contextMenu {
|
||||
width: 247px;
|
||||
|
||||
// These override the styles already present on the user menu rather than try to
|
||||
// define a new menu. They are specifically for the stacked menu when a community
|
||||
// is being represented as a prototype.
|
||||
&.mx_UserMenu_contextMenu_prototype {
|
||||
padding-bottom: 16px;
|
||||
|
||||
.mx_UserMenu_contextMenu_header {
|
||||
padding-bottom: 0;
|
||||
padding-top: 16px;
|
||||
|
||||
&:nth-child(n + 2) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 85%;
|
||||
opacity: 0.2;
|
||||
border: none;
|
||||
border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse
|
||||
}
|
||||
|
||||
&.mx_IconizedContextMenu {
|
||||
> .mx_IconizedContextMenu_optionList {
|
||||
margin-top: 4px;
|
||||
|
||||
&::before {
|
||||
border: none;
|
||||
}
|
||||
|
||||
> .mx_AccessibleButton {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_IconizedContextMenu .mx_IconizedContextMenu_optionList_red {
|
||||
.mx_AccessibleButton {
|
||||
padding-top: 16px;
|
||||
@ -193,4 +277,12 @@ limitations under the License.
|
||||
.mx_UserMenu_iconSignOut::before {
|
||||
mask-image: url('$(res)/img/element-icons/leave.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconMembers::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/members.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconInvite::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
}
|
||||
}
|
||||
|
77
res/css/views/dialogs/_EditCommunityPrototypeDialog.scss
Normal file
77
res/css/views/dialogs/_EditCommunityPrototypeDialog.scss
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// XXX: many of these styles are shared with the create dialog
|
||||
.mx_EditCommunityPrototypeDialog {
|
||||
&.mx_Dialog_fixedWidth {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.mx_Dialog_content {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.mx_AccessibleButton.mx_AccessibleButton_kind_primary {
|
||||
display: block;
|
||||
height: 32px;
|
||||
font-size: $font-16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_rowAvatar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_avatarContainer {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_avatar,
|
||||
.mx_EditCommunityPrototypeDialog_placeholderAvatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 96px;
|
||||
}
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_placeholderAvatar {
|
||||
background-color: #368bd6; // hardcoded for both themes
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
background-color: #fff; // hardcoded because the background is
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 96px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
mask-image: url('$(res)/img/element-icons/add-photo.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_tip {
|
||||
margin-left: 20px;
|
||||
|
||||
& > b, & > span {
|
||||
display: block;
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -89,6 +89,13 @@ limitations under the License.
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_subname {
|
||||
margin-bottom: 10px;
|
||||
margin-top: -10px; // HACK: Positioning with margins is bad
|
||||
font-size: $font-12px;
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_roomTile {
|
||||
@ -226,3 +233,7 @@ limitations under the License.
|
||||
.mx_InviteDialog_addressBar {
|
||||
margin-right: 45px;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import commonmark from 'commonmark';
|
||||
import escape from 'lodash/escape';
|
||||
import {escape} from "lodash";
|
||||
|
||||
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||
|
||||
|
@ -24,6 +24,7 @@ import * as sdk from './';
|
||||
import { _t } from './languageHandler';
|
||||
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
||||
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||
|
||||
/**
|
||||
* Invites multiple addresses to a room
|
||||
@ -64,6 +65,16 @@ export function showCommunityRoomInviteDialog(roomId, communityName) {
|
||||
);
|
||||
}
|
||||
|
||||
export function showCommunityInviteDialog(communityId) {
|
||||
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
|
||||
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import _clamp from 'lodash/clamp';
|
||||
import {clamp} from "lodash";
|
||||
|
||||
export default class SendHistoryManager {
|
||||
history: Array<HistoryItem> = [];
|
||||
@ -54,7 +54,7 @@ export default class SendHistoryManager {
|
||||
}
|
||||
|
||||
getItem(offset: number): ?HistoryItem {
|
||||
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
|
||||
this.currentIndex = clamp(this.currentIndex + offset, 0, this.history.length - 1);
|
||||
return this.history[this.currentIndex];
|
||||
}
|
||||
}
|
||||
|
@ -26,8 +26,9 @@ interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
|
||||
// Semantic component for representing a role=menuitem
|
||||
export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
|
||||
const ariaLabel = props["aria-label"] || label;
|
||||
return (
|
||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
|
||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}>
|
||||
{ children }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import * as sdk from '../index';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import {sortBy} from "lodash";
|
||||
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||
import FlairStore from "../stores/FlairStore";
|
||||
@ -81,7 +81,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
||||
|
||||
const matchedString = command[0];
|
||||
completions = this.matcher.match(matchedString);
|
||||
completions = _sortBy(completions, [
|
||||
completions = sortBy(completions, [
|
||||
(c) => score(matchedString, c.groupId),
|
||||
(c) => c.groupId.length,
|
||||
]).map(({avatarUrl, groupId, name}) => ({
|
||||
|
@ -23,8 +23,7 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import {ICompletion, ISelectionRange} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import {uniq, sortBy} from 'lodash';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||
import { EMOJI, IEmoji } from '../emoji';
|
||||
@ -115,7 +114,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||
}
|
||||
// Finally, sort by original ordering
|
||||
sorters.push((c) => c._orderBy);
|
||||
completions = _sortBy(_uniq(completions), sorters);
|
||||
completions = sortBy(uniq(completions), sorters);
|
||||
|
||||
completions = completions.map(({shortname}) => {
|
||||
const unicode = shortcodeToUnicode(shortname);
|
||||
|
@ -16,8 +16,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import _at from 'lodash/at';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import {at, uniq} from 'lodash';
|
||||
import {removeHiddenChars} from "matrix-js-sdk/src/utils";
|
||||
|
||||
interface IOptions<T extends {}> {
|
||||
@ -73,7 +72,7 @@ export default class QueryMatcher<T extends Object> {
|
||||
// type for their values. We assume that those values who's keys have
|
||||
// been specified will be string. Also, we cannot infer all the
|
||||
// types of the keys of the objects at compile.
|
||||
const keyValues = _at<string>(<any>object, this._options.keys);
|
||||
const keyValues = at<string>(<any>object, this._options.keys);
|
||||
|
||||
if (this._options.funcs) {
|
||||
for (const f of this._options.funcs) {
|
||||
@ -137,7 +136,7 @@ export default class QueryMatcher<T extends Object> {
|
||||
});
|
||||
|
||||
// Now map the keys to the result objects. Also remove any duplicates.
|
||||
return _uniq(matches.map((match) => match.object));
|
||||
return uniq(matches.map((match) => match.object));
|
||||
}
|
||||
|
||||
private processQuery(query: string): string {
|
||||
|
@ -27,7 +27,7 @@ import {PillCompletion} from './Components';
|
||||
import * as sdk from '../index';
|
||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||
import { uniqBy, sortBy } from 'lodash';
|
||||
import {uniqBy, sortBy} from "lodash";
|
||||
|
||||
const ROOM_REGEX = /\B#\S*/g;
|
||||
|
||||
|
@ -23,7 +23,7 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {PillCompletion} from './Components';
|
||||
import * as sdk from '../index';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import {sortBy} from 'lodash';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
|
||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
||||
@ -156,7 +156,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||
this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId);
|
||||
|
||||
this.users = _sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
|
||||
this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
|
||||
|
||||
this.matcher.setObjects(this.users);
|
||||
}
|
||||
|
@ -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")}
|
||||
/>,
|
||||
});
|
||||
|
||||
|
@ -1062,6 +1062,7 @@
|
||||
"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",
|
||||
"Invited": "Invited",
|
||||
"Filter room members": "Filter room members",
|
||||
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
||||
@ -1423,7 +1424,6 @@
|
||||
"Submit logs": "Submit logs",
|
||||
"Failed to load group members": "Failed to load group members",
|
||||
"Filter community members": "Filter community members",
|
||||
"Invite to this community": "Invite to this community",
|
||||
"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",
|
||||
@ -1684,6 +1684,8 @@
|
||||
"Verification Requests": "Verification Requests",
|
||||
"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",
|
||||
"An error has occurred.": "An error has occurred.",
|
||||
"Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.",
|
||||
"Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.",
|
||||
@ -1706,9 +1708,11 @@
|
||||
"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",
|
||||
"Direct Messages": "Direct Messages",
|
||||
"Start a conversation with someone using their name, username (like <userId/>) or email address.": "Start a conversation with someone using their name, username (like <userId/>) or email address.",
|
||||
"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>.": "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>.",
|
||||
"Go": "Go",
|
||||
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
|
||||
"a new master key signature": "a new master key signature",
|
||||
@ -2107,14 +2111,18 @@
|
||||
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
|
||||
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
|
||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||
"Switch to light mode": "Switch to light mode",
|
||||
"Switch to dark mode": "Switch to dark mode",
|
||||
"Switch theme": "Switch theme",
|
||||
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
|
||||
"Notification settings": "Notification settings",
|
||||
"Security & privacy": "Security & privacy",
|
||||
"All settings": "All settings",
|
||||
"Feedback": "Feedback",
|
||||
"Community settings": "Community settings",
|
||||
"User settings": "User settings",
|
||||
"Switch to light mode": "Switch to light mode",
|
||||
"Switch to dark mode": "Switch to dark mode",
|
||||
"Switch theme": "Switch theme",
|
||||
"User menu": "User menu",
|
||||
"Community and user menu": "Community and user menu",
|
||||
"Could not load user profile": "Could not load user profile",
|
||||
"Verify this login": "Verify this login",
|
||||
"Session verified": "Session verified",
|
||||
|
@ -26,7 +26,7 @@ limitations under the License.
|
||||
* on unmount or similar to cancel any pending update.
|
||||
*/
|
||||
|
||||
import { throttle } from "lodash";
|
||||
import {throttle} from "lodash";
|
||||
|
||||
export default function ratelimitedfunc(fn, time) {
|
||||
const throttledFn = throttle(fn, time, {
|
||||
|
@ -22,6 +22,11 @@ import { EffectiveMembership, getEffectiveMembership } from "../utils/membership
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import * as utils from "matrix-js-sdk/src/utils";
|
||||
import { UPDATE_EVENT } from "./AsyncStore";
|
||||
import FlairStore from "./FlairStore";
|
||||
import TagOrderStore from "./TagOrderStore";
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import GroupStore from "./GroupStore";
|
||||
import dis from "../dispatcher/dispatcher";
|
||||
|
||||
interface IState {
|
||||
// nothing of value - we use account data
|
||||
@ -43,6 +48,46 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
||||
return CommunityPrototypeStore.internalInstance;
|
||||
}
|
||||
|
||||
public getSelectedCommunityId(): string {
|
||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
return TagOrderStore.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 => MatrixClientPeg.get().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
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<any> {
|
||||
if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
return;
|
||||
@ -71,6 +116,15 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
||||
if (payload.event_type.startsWith("im.vector.group_info.")) {
|
||||
this.emit(UPDATE_EVENT, payload.event_type.substring("im.vector.group_info.".length));
|
||||
}
|
||||
} else if (payload.action === "select_tag") {
|
||||
// Automatically select the general chat when switching communities
|
||||
const chat = this.getGeneralChat(payload.tag);
|
||||
if (chat) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: chat.roomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,6 +148,23 @@ class FlairStore extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
|
@ -166,25 +166,6 @@ class TagOrderStore extends Store {
|
||||
selectedTags: newTags,
|
||||
});
|
||||
|
||||
if (!allowMultiple && newTags.length === 1) {
|
||||
// We're in prototype behaviour: select the general chat for the community
|
||||
const rooms = GroupStore.getGroupRooms(newTags[0])
|
||||
.map(r => MatrixClientPeg.get().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'] !== newTags[0]) return false;
|
||||
return true;
|
||||
});
|
||||
if (!chat) chat = rooms[0];
|
||||
if (chat) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: chat.roomId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.trackEvent('FilterStore', 'select_tag');
|
||||
}
|
||||
break;
|
||||
@ -285,13 +266,6 @@ class TagOrderStore extends Store {
|
||||
getSelectedTags() {
|
||||
return this._state.selectedTags;
|
||||
}
|
||||
|
||||
getSelectedPrototypeTag() {
|
||||
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||
return this.getSelectedTags()[0];
|
||||
}
|
||||
return null; // no selection as far as this function is concerned
|
||||
}
|
||||
}
|
||||
|
||||
if (global.singletonTagOrderStore === undefined) {
|
||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import {uniq} from "lodash";
|
||||
import {Room} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
/**
|
||||
@ -111,7 +111,7 @@ export default class DMRoomMap {
|
||||
userToRooms[userId] = [roomId];
|
||||
} else {
|
||||
roomIds.push(roomId);
|
||||
userToRooms[userId] = _uniq(roomIds);
|
||||
userToRooms[userId] = uniq(roomIds);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
|
Loading…
Reference in New Issue
Block a user