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/_CreateRoomDialog.scss";
|
||||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
|
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
@import "./views/dialogs/_InviteDialog.scss";
|
@import "./views/dialogs/_InviteDialog.scss";
|
||||||
|
@ -16,9 +16,33 @@ limitations under the License.
|
|||||||
|
|
||||||
.mx_UserMenu {
|
.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;
|
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 {
|
.mx_UserMenu_headerButtons {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@ -36,7 +60,7 @@ limitations under the License.
|
|||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
background: $primary-fg-color;
|
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 {
|
.mx_UserMenu_userName {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
@ -89,6 +135,44 @@ limitations under the License.
|
|||||||
.mx_UserMenu_contextMenu {
|
.mx_UserMenu_contextMenu {
|
||||||
width: 247px;
|
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_IconizedContextMenu .mx_IconizedContextMenu_optionList_red {
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
@ -193,4 +277,12 @@ limitations under the License.
|
|||||||
.mx_UserMenu_iconSignOut::before {
|
.mx_UserMenu_iconSignOut::before {
|
||||||
mask-image: url('$(res)/img/element-icons/leave.svg');
|
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;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
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 {
|
.mx_InviteDialog_roomTile {
|
||||||
@ -226,3 +233,7 @@ limitations under the License.
|
|||||||
.mx_InviteDialog_addressBar {
|
.mx_InviteDialog_addressBar {
|
||||||
margin-right: 45px;
|
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 commonmark from 'commonmark';
|
||||||
import escape from 'lodash/escape';
|
import {escape} from "lodash";
|
||||||
|
|
||||||
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import * as sdk from './';
|
|||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
||||||
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
||||||
|
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple addresses to a room
|
* 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.
|
* Checks if the given MatrixEvent is a valid 3rd party user invite.
|
||||||
* @param {MatrixEvent} event The event to check
|
* @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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import _clamp from 'lodash/clamp';
|
import {clamp} from "lodash";
|
||||||
|
|
||||||
export default class SendHistoryManager {
|
export default class SendHistoryManager {
|
||||||
history: Array<HistoryItem> = [];
|
history: Array<HistoryItem> = [];
|
||||||
@ -54,7 +54,7 @@ export default class SendHistoryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getItem(offset: number): ?HistoryItem {
|
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];
|
return this.history[this.currentIndex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,9 @@ interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
|||||||
|
|
||||||
// Semantic component for representing a role=menuitem
|
// Semantic component for representing a role=menuitem
|
||||||
export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
|
export const MenuItem: React.FC<IProps> = ({children, label, ...props}) => {
|
||||||
|
const ariaLabel = props["aria-label"] || label;
|
||||||
return (
|
return (
|
||||||
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={label}>
|
<AccessibleButton {...props} role="menuitem" tabIndex={-1} aria-label={ariaLabel}>
|
||||||
{ children }
|
{ children }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
@ -23,7 +23,7 @@ import {MatrixClientPeg} from '../MatrixClientPeg';
|
|||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import {sortBy} from "lodash";
|
||||||
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
import {makeGroupPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
import FlairStore from "../stores/FlairStore";
|
import FlairStore from "../stores/FlairStore";
|
||||||
@ -81,7 +81,7 @@ export default class CommunityProvider extends AutocompleteProvider {
|
|||||||
|
|
||||||
const matchedString = command[0];
|
const matchedString = command[0];
|
||||||
completions = this.matcher.match(matchedString);
|
completions = this.matcher.match(matchedString);
|
||||||
completions = _sortBy(completions, [
|
completions = sortBy(completions, [
|
||||||
(c) => score(matchedString, c.groupId),
|
(c) => score(matchedString, c.groupId),
|
||||||
(c) => c.groupId.length,
|
(c) => c.groupId.length,
|
||||||
]).map(({avatarUrl, groupId, name}) => ({
|
]).map(({avatarUrl, groupId, name}) => ({
|
||||||
|
@ -23,8 +23,7 @@ import AutocompleteProvider from './AutocompleteProvider';
|
|||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import {ICompletion, ISelectionRange} from './Autocompleter';
|
import {ICompletion, ISelectionRange} from './Autocompleter';
|
||||||
import _uniq from 'lodash/uniq';
|
import {uniq, sortBy} from 'lodash';
|
||||||
import _sortBy from 'lodash/sortBy';
|
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import { shortcodeToUnicode } from '../HtmlUtils';
|
import { shortcodeToUnicode } from '../HtmlUtils';
|
||||||
import { EMOJI, IEmoji } from '../emoji';
|
import { EMOJI, IEmoji } from '../emoji';
|
||||||
@ -115,7 +114,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||||||
}
|
}
|
||||||
// Finally, sort by original ordering
|
// Finally, sort by original ordering
|
||||||
sorters.push((c) => c._orderBy);
|
sorters.push((c) => c._orderBy);
|
||||||
completions = _sortBy(_uniq(completions), sorters);
|
completions = sortBy(uniq(completions), sorters);
|
||||||
|
|
||||||
completions = completions.map(({shortname}) => {
|
completions = completions.map(({shortname}) => {
|
||||||
const unicode = shortcodeToUnicode(shortname);
|
const unicode = shortcodeToUnicode(shortname);
|
||||||
|
@ -16,8 +16,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import _at from 'lodash/at';
|
import {at, uniq} from 'lodash';
|
||||||
import _uniq from 'lodash/uniq';
|
|
||||||
import {removeHiddenChars} from "matrix-js-sdk/src/utils";
|
import {removeHiddenChars} from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
interface IOptions<T extends {}> {
|
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
|
// type for their values. We assume that those values who's keys have
|
||||||
// been specified will be string. Also, we cannot infer all the
|
// been specified will be string. Also, we cannot infer all the
|
||||||
// types of the keys of the objects at compile.
|
// 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) {
|
if (this._options.funcs) {
|
||||||
for (const f of 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.
|
// 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 {
|
private processQuery(query: string): string {
|
||||||
|
@ -27,7 +27,7 @@ import {PillCompletion} from './Components';
|
|||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
||||||
import { uniqBy, sortBy } from 'lodash';
|
import {uniqBy, sortBy} from "lodash";
|
||||||
|
|
||||||
const ROOM_REGEX = /\B#\S*/g;
|
const ROOM_REGEX = /\B#\S*/g;
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ import AutocompleteProvider from './AutocompleteProvider';
|
|||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import {sortBy} from 'lodash';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
|
|
||||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
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;
|
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||||
this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId);
|
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);
|
this.matcher.setObjects(this.users);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import createReactClass from 'create-react-class';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Key } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { throttle } from 'lodash';
|
import {throttle} from 'lodash';
|
||||||
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
import AccessibleButton from '../../components/views/elements/AccessibleButton';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -42,6 +42,14 @@ import IconizedContextMenu, {
|
|||||||
IconizedContextMenuOption,
|
IconizedContextMenuOption,
|
||||||
IconizedContextMenuOptionList,
|
IconizedContextMenuOptionList,
|
||||||
} from "../views/context_menus/IconizedContextMenu";
|
} 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 {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
@ -58,6 +66,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private themeWatcherRef: string;
|
private themeWatcherRef: string;
|
||||||
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
private buttonRef: React.RefObject<HTMLButtonElement> = createRef();
|
||||||
|
private tagStoreRef: fbEmitter.EventSubscription;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -77,14 +86,20 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
|
||||||
|
this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef);
|
||||||
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
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 {
|
private isUserOnDarkTheme(): boolean {
|
||||||
const theme = SettingsStore.getValue("theme");
|
const theme = SettingsStore.getValue("theme");
|
||||||
if (theme.startsWith("custom-")) {
|
if (theme.startsWith("custom-")) {
|
||||||
@ -189,9 +204,54 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
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 => {
|
private renderContextMenu = (): React.ReactNode => {
|
||||||
if (!this.state.contextMenuPosition) return null;
|
if (!this.state.contextMenuPosition) return null;
|
||||||
|
|
||||||
|
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||||
|
|
||||||
let hostingLink;
|
let hostingLink;
|
||||||
const signupLink = getHostingLink("user-context-menu");
|
const signupLink = getHostingLink("user-context-menu");
|
||||||
if (signupLink) {
|
if (signupLink) {
|
||||||
@ -225,22 +285,137 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <IconizedContextMenu
|
let primaryHeader = (
|
||||||
// -20 to overlap the context menu by just over the width of the `...` icon and make it look connected
|
<div className="mx_UserMenu_contextMenu_name">
|
||||||
left={this.state.contextMenuPosition.width + this.state.contextMenuPosition.left - 20}
|
<span className="mx_UserMenu_contextMenu_displayName">
|
||||||
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
{OwnProfileStore.instance.displayName}
|
||||||
onFinished={this.onCloseMenu}
|
</span>
|
||||||
className="mx_UserMenu_contextMenu"
|
<span className="mx_UserMenu_contextMenu_userId">
|
||||||
>
|
{MatrixClientPeg.get().getUserId()}
|
||||||
<div className="mx_UserMenu_contextMenu_header">
|
</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">
|
<div className="mx_UserMenu_contextMenu_name">
|
||||||
<span className="mx_UserMenu_contextMenu_displayName">
|
<span className="mx_UserMenu_contextMenu_displayName">
|
||||||
{OwnProfileStore.instance.displayName}
|
{prototypeCommunityName}
|
||||||
</span>
|
|
||||||
<span className="mx_UserMenu_contextMenu_userId">
|
|
||||||
{MatrixClientPeg.get().getUserId()}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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
|
<AccessibleTooltipButton
|
||||||
className="mx_UserMenu_contextMenu_themeButton"
|
className="mx_UserMenu_contextMenu_themeButton"
|
||||||
onClick={this.onSwitchThemeClick}
|
onClick={this.onSwitchThemeClick}
|
||||||
@ -254,41 +429,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
</AccessibleTooltipButton>
|
</AccessibleTooltipButton>
|
||||||
</div>
|
</div>
|
||||||
{hostingLink}
|
{hostingLink}
|
||||||
<IconizedContextMenuOptionList>
|
{primaryOptionList}
|
||||||
{homeButton}
|
{secondarySection}
|
||||||
<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>
|
|
||||||
</IconizedContextMenu>;
|
</IconizedContextMenu>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -298,12 +440,34 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
|
const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
|
||||||
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
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 name = <span className="mx_UserMenu_userName">{displayName}</span>;
|
||||||
let buttons = (
|
let buttons = (
|
||||||
<span className="mx_UserMenu_headerButtons">
|
<span className="mx_UserMenu_headerButtons">
|
||||||
{/* masked image in CSS */}
|
{/* masked image in CSS */}
|
||||||
</span>
|
</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) {
|
if (this.props.isMinimized) {
|
||||||
name = null;
|
name = null;
|
||||||
buttons = null;
|
buttons = null;
|
||||||
@ -312,6 +476,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
'mx_UserMenu': true,
|
'mx_UserMenu': true,
|
||||||
'mx_UserMenu_minimized': this.props.isMinimized,
|
'mx_UserMenu_minimized': this.props.isMinimized,
|
||||||
|
'mx_UserMenu_prototype': isPrototype,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -320,7 +485,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||||||
className={classes}
|
className={classes}
|
||||||
onClick={this.onOpenMenuClick}
|
onClick={this.onOpenMenuClick}
|
||||||
inputRef={this.buttonRef}
|
inputRef={this.buttonRef}
|
||||||
label={_t("User menu")}
|
label={menuName}
|
||||||
isExpanded={!!this.state.contextMenuPosition}
|
isExpanded={!!this.state.contextMenuPosition}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
>
|
>
|
||||||
|
@ -25,8 +25,7 @@ import { _t } from '../../../languageHandler';
|
|||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
import {privateShouldBeEncrypted} from "../../../createRoom";
|
import {privateShouldBeEncrypted} from "../../../createRoom";
|
||||||
import TagOrderStore from "../../../stores/TagOrderStore";
|
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||||
import GroupStore from "../../../stores/GroupStore";
|
|
||||||
|
|
||||||
export default createReactClass({
|
export default createReactClass({
|
||||||
displayName: 'CreateRoomDialog',
|
displayName: 'CreateRoomDialog',
|
||||||
@ -72,8 +71,8 @@ export default createReactClass({
|
|||||||
opts.encryption = this.state.isEncrypted;
|
opts.encryption = this.state.isEncrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TagOrderStore.getSelectedPrototypeTag()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
opts.associatedWithCommunity = TagOrderStore.getSelectedPrototypeTag();
|
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts;
|
return opts;
|
||||||
@ -198,7 +197,7 @@ export default createReactClass({
|
|||||||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
||||||
"found and joined by anyone.",
|
"found and joined by anyone.",
|
||||||
)}</p>;
|
)}</p>;
|
||||||
if (TagOrderStore.getSelectedPrototypeTag()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
publicPrivateLabel = <p>{_t(
|
publicPrivateLabel = <p>{_t(
|
||||||
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
"Private rooms can be found and joined by invitation only. Public rooms can be " +
|
||||||
"found and joined by anyone in this community.",
|
"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');
|
let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
|
||||||
if (TagOrderStore.getSelectedPrototypeTag()) {
|
if (CommunityPrototypeStore.instance.getSelectedCommunityId()) {
|
||||||
const summary = GroupStore.getSummary(TagOrderStore.getSelectedPrototypeTag());
|
const name = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||||
const name = summary?.profile?.name || TagOrderStore.getSelectedPrototypeTag();
|
|
||||||
title = _t("Create a room in %(communityName)s", {communityName: name});
|
title = _t("Create a room in %(communityName)s", {communityName: name});
|
||||||
}
|
}
|
||||||
return (
|
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 Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import {humanizeTime} from "../../../utils/humanize";
|
||||||
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
|
import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
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.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
@ -909,12 +910,23 @@ export default class InviteDialog extends React.PureComponent {
|
|||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_onCommunityInviteClick = (e) => {
|
||||||
|
this.props.onFinished();
|
||||||
|
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||||
|
};
|
||||||
|
|
||||||
_renderSection(kind: "recents"|"suggestions") {
|
_renderSection(kind: "recents"|"suggestions") {
|
||||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||||
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
||||||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||||
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
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) {
|
if (this.props.kind === KIND_INVITE) {
|
||||||
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
||||||
@ -993,6 +1005,7 @@ export default class InviteDialog extends React.PureComponent {
|
|||||||
return (
|
return (
|
||||||
<div className='mx_InviteDialog_section'>
|
<div className='mx_InviteDialog_section'>
|
||||||
<h3>{sectionName}</h3>
|
<h3>{sectionName}</h3>
|
||||||
|
{sectionSubname ? <p className="mx_InviteDialog_subname">{sectionSubname}</p> : null}
|
||||||
{tiles}
|
{tiles}
|
||||||
{showMore}
|
{showMore}
|
||||||
</div>
|
</div>
|
||||||
@ -1083,6 +1096,33 @@ export default class InviteDialog extends React.PureComponent {
|
|||||||
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
|
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");
|
buttonText = _t("Go");
|
||||||
goButtonFn = this._startDm;
|
goButtonFn = this._startDm;
|
||||||
} else { // KIND_INVITE
|
} else { // KIND_INVITE
|
||||||
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { debounce } from 'lodash';
|
import {debounce} from "lodash";
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react';
|
import React, {InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes} from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { debounce } from 'lodash';
|
import {debounce} from "lodash";
|
||||||
import {IFieldState, IValidationResult} from "./Validation";
|
import {IFieldState, IValidationResult} from "./Validation";
|
||||||
|
|
||||||
// Invoke validation from user input (when typing, etc.) at most once every N ms.
|
// 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>', {}, {
|
_t('<a>In reply to</a> <pill>', {}, {
|
||||||
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyThread_show">{ sub }</a>,
|
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_ReplyThread_show">{ sub }</a>,
|
||||||
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
|
'pill': (
|
||||||
url={makeUserPermalink(ev.getSender())} shouldShowPillAvatar={true} />,
|
<Pill
|
||||||
|
type={Pill.TYPE_USER_MENTION}
|
||||||
|
room={room}
|
||||||
|
url={makeUserPermalink(ev.getSender())}
|
||||||
|
shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
|
||||||
|
/>
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</blockquote>;
|
</blockquote>;
|
||||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import React, {createRef, KeyboardEvent} from 'react';
|
import React, {createRef, KeyboardEvent} from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import flatMap from 'lodash/flatMap';
|
import {flatMap} from "lodash";
|
||||||
import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter';
|
import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter';
|
||||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
import {Room} from 'matrix-js-sdk/src/models/room';
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import rate_limited_func from "../../../ratelimitedfunc";
|
|||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import CallHandler from "../../../CallHandler";
|
import CallHandler from "../../../CallHandler";
|
||||||
|
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
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");
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
inviteButton =
|
inviteButton =
|
||||||
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!canInvite}>
|
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!canInvite}>
|
||||||
<span>{ _t('Invite to this room') }</span>
|
<span>{ inviteButtonText }</span>
|
||||||
</AccessibleButton>;
|
</AccessibleButton>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
|
|||||||
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
||||||
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import TagOrderStore from "../../../stores/TagOrderStore";
|
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||||
@ -130,7 +130,7 @@ const TAG_AESTHETICS: {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
label={TagOrderStore.getSelectedPrototypeTag()
|
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
|
||||||
? _t("Explore community rooms")
|
? _t("Explore community rooms")
|
||||||
: _t("Explore public rooms")}
|
: _t("Explore public rooms")}
|
||||||
iconClassName="mx_RoomList_iconExplore"
|
iconClassName="mx_RoomList_iconExplore"
|
||||||
|
@ -24,6 +24,7 @@ import {makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
|||||||
import BaseAvatar from "../avatars/BaseAvatar";
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
@replaceableComponent("views.settings.BridgeTile")
|
@replaceableComponent("views.settings.BridgeTile")
|
||||||
export default class BridgeTile extends React.PureComponent {
|
export default class BridgeTile extends React.PureComponent {
|
||||||
@ -56,7 +57,7 @@ export default class BridgeTile extends React.PureComponent {
|
|||||||
type={Pill.TYPE_USER_MENTION}
|
type={Pill.TYPE_USER_MENTION}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
url={makeUserPermalink(content.creator)}
|
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}
|
type={Pill.TYPE_USER_MENTION}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
url={makeUserPermalink(this.props.ev.getSender())}
|
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...|other": "and %(count)s others...",
|
||||||
"and %(count)s others...|one": "and one other...",
|
"and %(count)s others...|one": "and one other...",
|
||||||
"Invite to this room": "Invite to this room",
|
"Invite to this room": "Invite to this room",
|
||||||
|
"Invite to this community": "Invite to this community",
|
||||||
"Invited": "Invited",
|
"Invited": "Invited",
|
||||||
"Filter room members": "Filter room members",
|
"Filter room members": "Filter room members",
|
||||||
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
||||||
@ -1423,7 +1424,6 @@
|
|||||||
"Submit logs": "Submit logs",
|
"Submit logs": "Submit logs",
|
||||||
"Failed to load group members": "Failed to load group members",
|
"Failed to load group members": "Failed to load group members",
|
||||||
"Filter community members": "Filter community 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?",
|
"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.",
|
"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 room from community": "Failed to remove room from community",
|
||||||
@ -1684,6 +1684,8 @@
|
|||||||
"Verification Requests": "Verification Requests",
|
"Verification Requests": "Verification Requests",
|
||||||
"Toolbox": "Toolbox",
|
"Toolbox": "Toolbox",
|
||||||
"Developer Tools": "Developer Tools",
|
"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.",
|
"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.",
|
"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.",
|
"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",
|
"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",
|
"Recent Conversations": "Recent Conversations",
|
||||||
"Suggestions": "Suggestions",
|
"Suggestions": "Suggestions",
|
||||||
|
"May include members not in %(communityName)s": "May include members not in %(communityName)s",
|
||||||
"Recently Direct Messaged": "Recently Direct Messaged",
|
"Recently Direct Messaged": "Recently Direct Messaged",
|
||||||
"Direct Messages": "Direct Messages",
|
"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.": "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",
|
"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>.",
|
"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",
|
"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|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|zero": "Uploading %(filename)s",
|
||||||
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",
|
||||||
"Switch to light mode": "Switch to light mode",
|
"Failed to find the general chat for this community": "Failed to find the general chat for this community",
|
||||||
"Switch to dark mode": "Switch to dark mode",
|
|
||||||
"Switch theme": "Switch theme",
|
|
||||||
"Notification settings": "Notification settings",
|
"Notification settings": "Notification settings",
|
||||||
"Security & privacy": "Security & privacy",
|
"Security & privacy": "Security & privacy",
|
||||||
"All settings": "All settings",
|
"All settings": "All settings",
|
||||||
"Feedback": "Feedback",
|
"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",
|
"User menu": "User menu",
|
||||||
|
"Community and user menu": "Community and user menu",
|
||||||
"Could not load user profile": "Could not load user profile",
|
"Could not load user profile": "Could not load user profile",
|
||||||
"Verify this login": "Verify this login",
|
"Verify this login": "Verify this login",
|
||||||
"Session verified": "Session verified",
|
"Session verified": "Session verified",
|
||||||
|
@ -26,7 +26,7 @@ limitations under the License.
|
|||||||
* on unmount or similar to cancel any pending update.
|
* on unmount or similar to cancel any pending update.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { throttle } from "lodash";
|
import {throttle} from "lodash";
|
||||||
|
|
||||||
export default function ratelimitedfunc(fn, time) {
|
export default function ratelimitedfunc(fn, time) {
|
||||||
const throttledFn = throttle(fn, time, {
|
const throttledFn = throttle(fn, time, {
|
||||||
|
@ -22,6 +22,11 @@ import { EffectiveMembership, getEffectiveMembership } from "../utils/membership
|
|||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import * as utils from "matrix-js-sdk/src/utils";
|
import * as utils from "matrix-js-sdk/src/utils";
|
||||||
import { UPDATE_EVENT } from "./AsyncStore";
|
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 {
|
interface IState {
|
||||||
// nothing of value - we use account data
|
// nothing of value - we use account data
|
||||||
@ -43,6 +48,46 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
|||||||
return CommunityPrototypeStore.internalInstance;
|
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> {
|
protected async onAction(payload: ActionPayload): Promise<any> {
|
||||||
if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) {
|
||||||
return;
|
return;
|
||||||
@ -71,6 +116,15 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient<IState> {
|
|||||||
if (payload.event_type.startsWith("im.vector.group_info.")) {
|
if (payload.event_type.startsWith("im.vector.group_info.")) {
|
||||||
this.emit(UPDATE_EVENT, payload.event_type.substring("im.vector.group_info.".length));
|
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) {
|
async getGroupProfileCached(matrixClient, groupId) {
|
||||||
if (this._groupProfiles[groupId]) {
|
if (this._groupProfiles[groupId]) {
|
||||||
return this._groupProfiles[groupId];
|
return this._groupProfiles[groupId];
|
||||||
|
@ -166,25 +166,6 @@ class TagOrderStore extends Store {
|
|||||||
selectedTags: newTags,
|
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');
|
Analytics.trackEvent('FilterStore', 'select_tag');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -285,13 +266,6 @@ class TagOrderStore extends Store {
|
|||||||
getSelectedTags() {
|
getSelectedTags() {
|
||||||
return this._state.selectedTags;
|
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) {
|
if (global.singletonTagOrderStore === undefined) {
|
||||||
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
import _uniq from 'lodash/uniq';
|
import {uniq} from "lodash";
|
||||||
import {Room} from "matrix-js-sdk/src/matrix";
|
import {Room} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -111,7 +111,7 @@ export default class DMRoomMap {
|
|||||||
userToRooms[userId] = [roomId];
|
userToRooms[userId] = [roomId];
|
||||||
} else {
|
} else {
|
||||||
roomIds.push(roomId);
|
roomIds.push(roomId);
|
||||||
userToRooms[userId] = _uniq(roomIds);
|
userToRooms[userId] = uniq(roomIds);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
Loading…
Reference in New Issue
Block a user