From 2c20afc04729f157698a51471d47e4b1edd66fff Mon Sep 17 00:00:00 2001 From: resynth1943 Date: Wed, 26 Aug 2020 23:04:56 +0100 Subject: [PATCH 01/22] Fix lodash imports Signed-off-by: resynth1943 --- src/Markdown.js | 2 +- src/SendHistoryManager.js | 2 +- src/autocomplete/CommunityProvider.tsx | 2 +- src/autocomplete/RoomProvider.tsx | 3 ++- src/components/structures/RoomSearch.tsx | 1 - src/components/structures/SearchBox.js | 2 +- .../views/dialogs/secretstorage/AccessSecretStorageDialog.js | 2 +- src/components/views/elements/Field.tsx | 2 +- src/components/views/rooms/Autocomplete.tsx | 2 +- src/emojipicker/recent.ts | 2 +- src/ratelimitedfunc.js | 2 +- src/stores/CustomRoomTagStore.js | 2 +- src/stores/OwnProfileStore.ts | 2 +- src/stores/room-list/filters/NameFilterCondition.ts | 2 +- src/utils/DMRoomMap.js | 2 +- src/utils/ResizeNotifier.js | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index e57507b4de..7a573996c3 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -15,7 +15,7 @@ limitations under the License. */ import commonmark from 'commonmark'; -import escape from 'lodash/escape'; +import escape from "lodash/escape"; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; diff --git a/src/SendHistoryManager.js b/src/SendHistoryManager.js index 794a58ad6f..d38227ae76 100644 --- a/src/SendHistoryManager.js +++ b/src/SendHistoryManager.js @@ -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/clamp"; export default class SendHistoryManager { history: Array = []; diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index f34fee890e..e2baa29026 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -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/sortBy"; import {makeGroupPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index b18b2d132c..30a322d6b1 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -27,7 +27,8 @@ 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 from "lodash/uniqBy"; +import sortBy from "lodash/sortBy"; const ROOM_REGEX = /\B#\S*/g; diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index f6b8d42c30..fc8b7a3a12 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -20,7 +20,6 @@ import classNames from "classnames"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import { ActionPayload } from "../../dispatcher/payloads"; -import { throttle } from 'lodash'; import { Key } from "../../Keyboard"; import AccessibleButton from "../views/elements/AccessibleButton"; import { Action } from "../../dispatcher/actions"; diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 7e9d290bce..ef0ee38ce6 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -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/throttle'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; import classNames from 'classnames'; diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 5c01a6907f..2d556aa597 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -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/debounce"; import classNames from 'classnames'; import React from 'react'; import PropTypes from "prop-types"; diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index d9fd59dc11..935d452aaa 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -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/debounce"; import {IFieldState, IValidationResult} from "./Validation"; // Invoke validation from user input (when typing, etc.) at most once every N ms. diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 70f7556550..8fa623bd91 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -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/flatMap"; import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter'; import {Room} from 'matrix-js-sdk/src/models/room'; diff --git a/src/emojipicker/recent.ts b/src/emojipicker/recent.ts index d86aad660d..0f005214a3 100644 --- a/src/emojipicker/recent.ts +++ b/src/emojipicker/recent.ts @@ -16,7 +16,7 @@ limitations under the License. */ import SettingsStore from "../settings/SettingsStore"; -import {orderBy} from "lodash"; +import orderBy from "lodash/orderBy"; import { SettingLevel } from "../settings/SettingLevel"; interface ILegacyFormat { diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js index 1f15f11d91..691671a086 100644 --- a/src/ratelimitedfunc.js +++ b/src/ratelimitedfunc.js @@ -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/throttle"; export default function ratelimitedfunc(fn, time) { const throttledFn = throttle(fn, time, { diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 39177181b4..ceee67c1e1 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -17,7 +17,7 @@ limitations under the License. import dis from '../dispatcher/dispatcher'; import EventEmitter from 'events'; -import {throttle} from "lodash"; +import throttle from "lodash/throttle"; import SettingsStore from "../settings/SettingsStore"; import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore"; import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore"; diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts index 1aa761e1c4..af615b07b6 100644 --- a/src/stores/OwnProfileStore.ts +++ b/src/stores/OwnProfileStore.ts @@ -19,7 +19,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { User } from "matrix-js-sdk/src/models/user"; -import { throttle } from "lodash"; +import throttle from "lodash/throttle"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 88edaecfb6..13c75f3d4d 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition"; import { EventEmitter } from "events"; import { removeHiddenChars } from "matrix-js-sdk/src/utils"; -import { throttle } from "lodash"; +import throttle from "lodash/throttle"; /** * A filter condition for the room list which reveals rooms of a particular diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js index 6ce92a0458..c6ba0d4d83 100644 --- a/src/utils/DMRoomMap.js +++ b/src/utils/DMRoomMap.js @@ -16,7 +16,7 @@ limitations under the License. */ import {MatrixClientPeg} from '../MatrixClientPeg'; -import _uniq from 'lodash/uniq'; +import _uniq from "lodash/uniq"; import {Room} from "matrix-js-sdk/src/matrix"; /** diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js index 5467716576..c43e2c8250 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.js @@ -23,7 +23,7 @@ limitations under the License. * @event module:utils~ResizeNotifier#"middlePanelResizedNoisy" */ import { EventEmitter } from "events"; -import { throttle } from "lodash"; +import throttle from "lodash/throttle"; export default class ResizeNotifier extends EventEmitter { constructor() { From cc2fc911afc82915067bc6ee5790e419dd6d83dc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 10:03:27 -0600 Subject: [PATCH 02/22] Minor copy addition to DM dialog for communities prototype --- res/css/views/dialogs/_InviteDialog.scss | 7 +++++++ src/components/views/dialogs/InviteDialog.js | 10 ++++++++++ src/i18n/strings/en_EN.json | 1 + 3 files changed, 18 insertions(+) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index a77d0bfbba..a0f98d74e9 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -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 { diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 6cd0b22505..ba4abb31a7 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -37,6 +37,8 @@ import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; import {DefaultTagID} from "../../../stores/room-list/models"; import RoomListStore from "../../../stores/room-list/RoomListStore"; +import TagOrderStore from "../../../stores/TagOrderStore"; +import GroupStore from "../../../stores/GroupStore"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -915,6 +917,13 @@ export default class InviteDialog extends React.PureComponent { 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' && TagOrderStore.getSelectedPrototypeTag()) { + const summary = GroupStore.getSummary(TagOrderStore.getSelectedPrototypeTag()); + const communityName = summary?.profile?.name || TagOrderStore.getSelectedPrototypeTag(); + 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 +1002,7 @@ export default class InviteDialog extends React.PureComponent { return (

{sectionName}

+ {sectionSubname ?

{sectionSubname}

: null} {tiles} {showMore}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 442f07499c..2a914641f1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1709,6 +1709,7 @@ "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 ) or email address.": "Start a conversation with someone using their name, username (like ) or email address.", From 1fc55b33c1d03423395ad5d149a8796d0fe4f52b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 28 Aug 2020 18:53:43 +0100 Subject: [PATCH 03/22] Stash lodash import optimization --- src/Markdown.js | 2 +- src/SendHistoryManager.js | 4 ++-- src/autocomplete/CommunityProvider.tsx | 4 ++-- src/autocomplete/EmojiProvider.tsx | 5 ++--- src/autocomplete/QueryMatcher.ts | 7 +++---- src/autocomplete/RoomProvider.tsx | 3 +-- src/autocomplete/UserProvider.tsx | 4 ++-- src/components/structures/SearchBox.js | 2 +- .../dialogs/secretstorage/AccessSecretStorageDialog.js | 2 +- src/components/views/elements/Field.tsx | 2 +- src/components/views/rooms/Autocomplete.tsx | 2 +- src/emojipicker/recent.ts | 2 +- src/ratelimitedfunc.js | 2 +- src/stores/CustomRoomTagStore.js | 2 +- src/stores/OwnProfileStore.ts | 2 +- src/stores/room-list/filters/NameFilterCondition.ts | 2 +- src/utils/DMRoomMap.js | 4 ++-- src/utils/ResizeNotifier.js | 2 +- 18 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/Markdown.js b/src/Markdown.js index 7a573996c3..492450e87d 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -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']; diff --git a/src/SendHistoryManager.js b/src/SendHistoryManager.js index d38227ae76..d9955727a4 100644 --- a/src/SendHistoryManager.js +++ b/src/SendHistoryManager.js @@ -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 = []; @@ -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]; } } diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index e2baa29026..43217cc1bb 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -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}) => ({ diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 147d68f5ff..1ccbc41bd6 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -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); diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 9c91414556..a07ed29c7e 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -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 { @@ -73,7 +72,7 @@ export default class QueryMatcher { // 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(object, this._options.keys); + const keyValues = at(object, this._options.keys); if (this._options.funcs) { for (const f of this._options.funcs) { @@ -137,7 +136,7 @@ export default class QueryMatcher { }); // 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 { diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 30a322d6b1..de16e68e01 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -27,8 +27,7 @@ import {PillCompletion} from './Components'; import * as sdk from '../index'; import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import {ICompletion, ISelectionRange} from "./Autocompleter"; -import uniqBy from "lodash/uniqBy"; -import sortBy from "lodash/sortBy"; +import {uniqBy, sortBy} from "lodash"; const ROOM_REGEX = /\B#\S*/g; diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index c957b5e597..d592421ef2 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -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"; @@ -151,7 +151,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); } diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index ef0ee38ce6..e4befee863 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -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/throttle'; +import {throttle} from 'lodash'; import AccessibleButton from '../../components/views/elements/AccessibleButton'; import classNames from 'classnames'; diff --git a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js index 2d556aa597..85ace249a3 100644 --- a/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js +++ b/src/components/views/dialogs/secretstorage/AccessSecretStorageDialog.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import debounce from "lodash/debounce"; +import {debounce} from "lodash"; import classNames from 'classnames'; import React from 'react'; import PropTypes from "prop-types"; diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 935d452aaa..8c4e6aed31 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -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/debounce"; +import {debounce} from "lodash"; import {IFieldState, IValidationResult} from "./Validation"; // Invoke validation from user input (when typing, etc.) at most once every N ms. diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index 8fa623bd91..15af75084a 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -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'; diff --git a/src/emojipicker/recent.ts b/src/emojipicker/recent.ts index 0f005214a3..d86aad660d 100644 --- a/src/emojipicker/recent.ts +++ b/src/emojipicker/recent.ts @@ -16,7 +16,7 @@ limitations under the License. */ import SettingsStore from "../settings/SettingsStore"; -import orderBy from "lodash/orderBy"; +import {orderBy} from "lodash"; import { SettingLevel } from "../settings/SettingLevel"; interface ILegacyFormat { diff --git a/src/ratelimitedfunc.js b/src/ratelimitedfunc.js index 691671a086..3df3db615e 100644 --- a/src/ratelimitedfunc.js +++ b/src/ratelimitedfunc.js @@ -26,7 +26,7 @@ limitations under the License. * on unmount or similar to cancel any pending update. */ -import throttle from "lodash/throttle"; +import {throttle} from "lodash"; export default function ratelimitedfunc(fn, time) { const throttledFn = throttle(fn, time, { diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index ceee67c1e1..39177181b4 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -17,7 +17,7 @@ limitations under the License. import dis from '../dispatcher/dispatcher'; import EventEmitter from 'events'; -import throttle from "lodash/throttle"; +import {throttle} from "lodash"; import SettingsStore from "../settings/SettingsStore"; import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore"; import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore"; diff --git a/src/stores/OwnProfileStore.ts b/src/stores/OwnProfileStore.ts index af615b07b6..1aa761e1c4 100644 --- a/src/stores/OwnProfileStore.ts +++ b/src/stores/OwnProfileStore.ts @@ -19,7 +19,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { User } from "matrix-js-sdk/src/models/user"; -import throttle from "lodash/throttle"; +import { throttle } from "lodash"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 13c75f3d4d..88edaecfb6 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition"; import { EventEmitter } from "events"; import { removeHiddenChars } from "matrix-js-sdk/src/utils"; -import throttle from "lodash/throttle"; +import { throttle } from "lodash"; /** * A filter condition for the room list which reveals rooms of a particular diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js index c6ba0d4d83..4e219b1611 100644 --- a/src/utils/DMRoomMap.js +++ b/src/utils/DMRoomMap.js @@ -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; diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.js index c43e2c8250..5467716576 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.js @@ -23,7 +23,7 @@ limitations under the License. * @event module:utils~ResizeNotifier#"middlePanelResizedNoisy" */ import { EventEmitter } from "events"; -import throttle from "lodash/throttle"; +import { throttle } from "lodash"; export default class ResizeNotifier extends EventEmitter { constructor() { From e58b514803d934005434ae1745fcfddb677d73d3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 13:22:20 -0600 Subject: [PATCH 04/22] Add clarifying text to DM dialog about what is about to happen --- res/css/views/dialogs/_InviteDialog.scss | 4 ++++ src/RoomInvite.js | 20 +++++++++++++++++ src/components/views/dialogs/InviteDialog.js | 23 +++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index a0f98d74e9..b9063f46b9 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -233,3 +233,7 @@ limitations under the License. .mx_InviteDialog_addressBar { margin-right: 45px; } + +.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { + padding: 0; +} diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 420561ea41..408b4c9c6c 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -24,6 +24,8 @@ 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 GroupStore from "./stores/GroupStore"; +import dis from "./dispatcher/dispatcher"; /** * Invites multiple addresses to a room @@ -64,6 +66,24 @@ export function showCommunityRoomInviteDialog(roomId, communityName) { ); } +export function showCommunityInviteDialog(communityId) { + 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]; + if (chat) { + const summary = GroupStore.getSummary(communityId); + showCommunityRoomInviteDialog(chat.roomId, summary?.profile?.name || communityId); + } 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 diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index ba4abb31a7..b70f2fee10 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -32,7 +32,7 @@ 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"; @@ -911,6 +911,11 @@ export default class InviteDialog extends React.PureComponent { this.props.onFinished(); }; + _onCommunityInviteClick = (e) => { + this.props.onFinished(); + showCommunityInviteDialog(TagOrderStore.getSelectedPrototypeTag()); + }; + _renderSection(kind: "recents"|"suggestions") { let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions; let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown; @@ -1093,6 +1098,22 @@ export default class InviteDialog extends React.PureComponent { return {userId}; }}, ); + if (TagOrderStore.getSelectedPrototypeTag()) { + const communityId = TagOrderStore.getSelectedPrototypeTag(); + const communityName = GroupStore.getSummary(communityId)?.profile?.name || communityId; + helpText = _t( + "Start a conversation with someone using their name, username (like ) or email address. " + + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here.", + {communityName}, { + userId: () => { + return {userId}; + }, + a: (sub) => { + return {sub} + }, + }, + ) + } buttonText = _t("Go"); goButtonFn = this._startDm; } else { // KIND_INVITE diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2a914641f1..0d01fd47c8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1713,6 +1713,7 @@ "Recently Direct Messaged": "Recently Direct Messaged", "Direct Messages": "Direct Messages", "Start a conversation with someone using their name, username (like ) or email address.": "Start a conversation with someone using their name, username (like ) or email address.", + "Start a conversation with someone using their name, username (like ) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here.": "Start a conversation with someone using their name, username (like ) or email address. This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here.", "Go": "Go", "Invite someone using their name, username (like ), email address or share this room.": "Invite someone using their name, username (like ), email address or share this room.", "a new master key signature": "a new master key signature", From 6f237161fd841ee181fd38bfd3b8452020494c05 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 13:28:54 -0600 Subject: [PATCH 05/22] Appease the linter --- src/components/views/dialogs/InviteDialog.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index b70f2fee10..934ed12ac1 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -1103,16 +1103,28 @@ export default class InviteDialog extends React.PureComponent { const communityName = GroupStore.getSummary(communityId)?.profile?.name || communityId; helpText = _t( "Start a conversation with someone using their name, username (like ) or email address. " + - "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here.", + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click " + + "here.", {communityName}, { userId: () => { - return {userId}; + return ( + {userId} + ); }, a: (sub) => { - return {sub} + return ( + {sub} + ); }, }, - ) + ); } buttonText = _t("Go"); goButtonFn = this._startDm; From fb54b62be99ba4ae1f2c2a2bf9ffd448aeab72a2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 13:42:19 -0600 Subject: [PATCH 06/22] Appease the rest of the linter --- src/RoomInvite.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 408b4c9c6c..cae4501901 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -25,7 +25,6 @@ import { _t } from './languageHandler'; import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import GroupStore from "./stores/GroupStore"; -import dis from "./dispatcher/dispatcher"; /** * Invites multiple addresses to a room From 90d9d7128d2ccd7767aee6a8ef4053f38174fdd4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 14:56:59 -0600 Subject: [PATCH 07/22] Use FlairStore's cache for group naming Turns out GroupStore doesn't really know much. --- src/RoomInvite.js | 4 ++-- src/components/views/dialogs/CreateRoomDialog.js | 5 ++--- src/components/views/dialogs/InviteDialog.js | 8 +++----- src/stores/CommunityPrototypeStore.ts | 11 +++++++++++ src/stores/FlairStore.js | 16 ++++++++++++++++ 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index cae4501901..7f2eec32f3 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -76,8 +76,8 @@ export function showCommunityInviteDialog(communityId) { }); if (!chat) chat = rooms[0]; if (chat) { - const summary = GroupStore.getSummary(communityId); - showCommunityRoomInviteDialog(chat.roomId, summary?.profile?.name || communityId); + const name = CommunityPrototypeInviteDialog.instance.getCommunityName(communityId); + showCommunityRoomInviteDialog(chat.roomId, name); } else { throw new Error("Failed to locate appropriate room to start an invite in"); } diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index 4890626527..bdd3de07c0 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -26,7 +26,7 @@ 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', @@ -240,8 +240,7 @@ 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(); + const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); title = _t("Create a room in %(communityName)s", {communityName: name}); } return ( diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 934ed12ac1..c2fd7e5b0e 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -38,7 +38,7 @@ import {Action} from "../../../dispatcher/actions"; import {DefaultTagID} from "../../../stores/room-list/models"; import RoomListStore from "../../../stores/room-list/RoomListStore"; import TagOrderStore from "../../../stores/TagOrderStore"; -import GroupStore from "../../../stores/GroupStore"; +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 */ @@ -925,8 +925,7 @@ export default class InviteDialog extends React.PureComponent { let sectionSubname = null; if (kind === 'suggestions' && TagOrderStore.getSelectedPrototypeTag()) { - const summary = GroupStore.getSummary(TagOrderStore.getSelectedPrototypeTag()); - const communityName = summary?.profile?.name || TagOrderStore.getSelectedPrototypeTag(); + const communityName = CommunityPrototypeStore.instance.getCommunityName(TagOrderStore.getSelectedPrototypeTag()); sectionSubname = _t("May include members not in %(communityName)s", {communityName}); } @@ -1099,8 +1098,7 @@ export default class InviteDialog extends React.PureComponent { }}, ); if (TagOrderStore.getSelectedPrototypeTag()) { - const communityId = TagOrderStore.getSelectedPrototypeTag(); - const communityName = GroupStore.getSummary(communityId)?.profile?.name || communityId; + const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); helpText = _t( "Start a conversation with someone using their name, username (like ) or email address. " + "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click " + diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 581f8a97c8..eec0a8aab8 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -22,6 +22,8 @@ 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"; interface IState { // nothing of value - we use account data @@ -43,6 +45,15 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { return CommunityPrototypeStore.internalInstance; } + public getSelectedCommunityName(): string { + return CommunityPrototypeStore.instance.getCommunityName(TagOrderStore.getSelectedPrototypeTag()); + } + + public getCommunityName(communityId: string): string { + const profile = FlairStore.getGroupProfileCachedFast(this.matrixClient, communityId); + return profile?.name || communityId; + } + protected async onAction(payload: ActionPayload): Promise { if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) { return; diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 94b81c1ba5..10a4d96921 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -148,6 +148,22 @@ 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 The matrix client to use to fetch the profile, if needed. + * @param groupId The group ID to get the profile for. + * @returns The profile if known, otherwise null. + */ + getGroupProfileCachedFast(matrixClient, groupId) { + 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]; From 0ffa5488647a29682fe91391126eee3d2703ba09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 15:37:23 -0600 Subject: [PATCH 08/22] Change the menu button to a chevron by design request --- res/css/structures/_UserMenu.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 78795c85a2..b4e3a08e18 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -16,7 +16,7 @@ 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_headerButtons { @@ -36,7 +36,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'); } } From 01b0acbe62c2ad2d3113df7d92957ce33b55cb9c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 16:14:08 -0600 Subject: [PATCH 09/22] Make the UserMenu echo the current community name --- res/css/structures/_UserMenu.scss | 46 ++++++++++++++++++++++++++ src/components/structures/UserMenu.tsx | 35 +++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index b4e3a08e18..08fb1f49f0 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -19,6 +19,30 @@ limitations under the License. // 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 $roomsublist-divider-color; + opacity: 0.2; + display: block; + padding-top: 8px; + } + } + .mx_UserMenu_headerButtons { width: 16px; height: 16px; @@ -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; diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 30be71abcb..a1039f23b9 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -42,6 +42,9 @@ import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../views/context_menus/IconizedContextMenu"; +import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; +import * as fbEmitter from "fbemitter"; +import TagOrderStore from "../../stores/TagOrderStore"; interface IProps { isMinimized: boolean; @@ -58,6 +61,7 @@ export default class UserMenu extends React.Component { private dispatcherRef: string; private themeWatcherRef: string; private buttonRef: React.RefObject = createRef(); + private tagStoreRef: fbEmitter.EventSubscription; constructor(props: IProps) { super(props); @@ -77,14 +81,20 @@ export default class UserMenu extends React.Component { 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-")) { @@ -298,12 +308,34 @@ export default class UserMenu extends React.Component { 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 = {displayName}; let buttons = ( {/* masked image in CSS */} ); + if (prototypeCommunityName) { + name = ( +
+ {prototypeCommunityName} + {displayName} +
+ ); + menuName = _t("Community and user menu"); + isPrototype = true; + } else if (SettingsStore.getValue("feature_communities_v2_prototypes")) { + name = ( +
+ {_t("Home")} + {displayName} +
+ ); + isPrototype = true; + } if (this.props.isMinimized) { name = null; buttons = null; @@ -312,6 +344,7 @@ export default class UserMenu extends React.Component { const classes = classNames({ 'mx_UserMenu': true, 'mx_UserMenu_minimized': this.props.isMinimized, + 'mx_UserMenu_prototype': isPrototype, }); return ( @@ -320,7 +353,7 @@ export default class UserMenu extends React.Component { className={classes} onClick={this.onOpenMenuClick} inputRef={this.buttonRef} - label={_t("User menu")} + label={menuName} isExpanded={!!this.state.contextMenuPosition} onContextMenu={this.onContextMenu} > diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0d01fd47c8..3589c2ba76 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2129,6 +2129,7 @@ "All settings": "All settings", "Feedback": "Feedback", "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", From 02095389e7b82ceeaba192e6099689d93eada97d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 17:03:17 -0600 Subject: [PATCH 10/22] Add structure for mixed prototype UserMenu --- res/css/structures/_UserMenu.scss | 46 ++++++ src/components/structures/UserMenu.tsx | 197 +++++++++++++++++++------ src/i18n/strings/en_EN.json | 6 +- 3 files changed, 199 insertions(+), 50 deletions(-) diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 08fb1f49f0..8c944935ed 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -135,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 $roomsublist-divider-color; + } + + &.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; @@ -239,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'); + } } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index a1039f23b9..5db5371842 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -199,9 +199,32 @@ export default class UserMenu extends React.Component { defaultDispatcher.dispatch({action: 'view_home_page'}); }; + private onCommunitySettingsClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO@onCommunitySettingsClick"); + }; + + private onCommunityMembersClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO@onCommunityMembersClick"); + }; + + private onCommunityInviteClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO@onCommunityInviteClick"); + }; + 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) { @@ -235,22 +258,135 @@ export default class UserMenu extends React.Component { ); } + let primaryHeader = ( +
+ + {OwnProfileStore.instance.displayName} + + + {MatrixClientPeg.get().getUserId()} + +
+ ); + let primaryOptionList = ( + + + {homeButton} + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} + /> + this.onSettingsOpen(e, USER_SECURITY_TAB)} + /> + this.onSettingsOpen(e, null)} + /> + {/* */} + + + + + + + ); + let secondarySection = null; + + if (prototypeCommunityName) { + primaryHeader = ( +
+ + {prototypeCommunityName} + +
+ ); + primaryOptionList = ( + + + + + + ); + secondarySection = ( + +
+
+
+ + {OwnProfileStore.instance.displayName} + + + {MatrixClientPeg.get().getUserId()} + +
+
+ + this.onSettingsOpen(e, null)} + /> + + + + + +
+ ) + } + + const classes = classNames({ + "mx_UserMenu_contextMenu": true, + "mx_UserMenu_contextMenu_prototype": !!prototypeCommunityName, + }); + return
-
- - {OwnProfileStore.instance.displayName} - - - {MatrixClientPeg.get().getUserId()} - -
+ {primaryHeader} {
{hostingLink} - - {homeButton} - this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} - /> - this.onSettingsOpen(e, USER_SECURITY_TAB)} - /> - this.onSettingsOpen(e, null)} - /> - {/* */} - - - - - + {primaryOptionList} + {secondarySection}
; }; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3589c2ba76..39b6061f27 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2121,13 +2121,13 @@ "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", "Notification settings": "Notification settings", "Security & privacy": "Security & privacy", "All settings": "All settings", "Feedback": "Feedback", + "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", From 35e4d89545edbf4ee7c57558dce2518f5e9c89c0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 20:04:19 -0600 Subject: [PATCH 11/22] Add aria labels to menu options --- src/accessibility/context_menu/MenuItem.tsx | 3 ++- src/components/structures/UserMenu.tsx | 2 ++ src/i18n/strings/en_EN.json | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/accessibility/context_menu/MenuItem.tsx b/src/accessibility/context_menu/MenuItem.tsx index 64233e51ad..0bb169abf8 100644 --- a/src/accessibility/context_menu/MenuItem.tsx +++ b/src/accessibility/context_menu/MenuItem.tsx @@ -26,8 +26,9 @@ interface IProps extends React.ComponentProps { // Semantic component for representing a role=menuitem export const MenuItem: React.FC = ({children, label, ...props}) => { + const ariaLabel = props["aria-label"] || label; return ( - + { children } ); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 5db5371842..476fe19ad7 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -322,6 +322,7 @@ export default class UserMenu extends React.Component { { this.onSettingsOpen(e, null)} /> Date: Fri, 28 Aug 2020 20:08:12 -0600 Subject: [PATCH 12/22] Wire up the invite button --- src/RoomInvite.js | 3 ++- src/components/structures/UserMenu.tsx | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 7f2eec32f3..ed3fe1452e 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -25,6 +25,7 @@ import { _t } from './languageHandler'; import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import GroupStore from "./stores/GroupStore"; +import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; /** * Invites multiple addresses to a room @@ -76,7 +77,7 @@ export function showCommunityInviteDialog(communityId) { }); if (!chat) chat = rooms[0]; if (chat) { - const name = CommunityPrototypeInviteDialog.instance.getCommunityName(communityId); + const name = CommunityPrototypeStore.instance.getCommunityName(communityId); showCommunityRoomInviteDialog(chat.roomId, name); } else { throw new Error("Failed to locate appropriate room to start an invite in"); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 476fe19ad7..8e62402141 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -45,6 +45,7 @@ import IconizedContextMenu, { import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import * as fbEmitter from "fbemitter"; import TagOrderStore from "../../stores/TagOrderStore"; +import { showCommunityInviteDialog } from "../../RoomInvite"; interface IProps { isMinimized: boolean; @@ -217,7 +218,8 @@ export default class UserMenu extends React.Component { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO@onCommunityInviteClick"); + showCommunityInviteDialog(TagOrderStore.getSelectedPrototypeTag()); + this.setState({contextMenuPosition: null}); // also close the menu }; private renderContextMenu = (): React.ReactNode => { From 281e2ab27bcf44a230020e14b73e202afd66dd4b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 20:13:26 -0600 Subject: [PATCH 13/22] Null guard new function to reduce error spam --- src/stores/FlairStore.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 10a4d96921..67d9616741 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -157,6 +157,7 @@ class FlairStore extends EventEmitter { * @returns The profile if known, otherwise null. */ getGroupProfileCachedFast(matrixClient, groupId) { + if (!matrixClient || !groupId) return null; if (this._groupProfiles[groupId]) { return this._groupProfiles[groupId]; } From 93d67a668943a6e511e1735041e8a3b31a1e6bd9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 Aug 2020 10:12:12 -0600 Subject: [PATCH 14/22] Wire up members button to member view Ideally this would open up the group members panel, but that's exceedingly difficult. Instead, we switch to the general chat and rename the button to be a bit more helpful. --- src/RoomInvite.js | 10 +--------- src/components/structures/UserMenu.tsx | 23 ++++++++++++++++++++++- src/components/views/rooms/MemberList.js | 13 ++++++++++++- src/i18n/strings/en_EN.json | 1 + src/stores/CommunityPrototypeStore.ts | 15 +++++++++++++++ src/stores/TagOrderStore.js | 1 + 6 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index ed3fe1452e..b82cc0a8e7 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -67,15 +67,7 @@ export function showCommunityRoomInviteDialog(roomId, communityName) { } export function showCommunityInviteDialog(communityId) { - 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]; + const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId); if (chat) { const name = CommunityPrototypeStore.instance.getCommunityName(communityId); showCommunityRoomInviteDialog(chat.roomId, name); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 8e62402141..a583af2603 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -46,6 +46,9 @@ 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"; interface IProps { isMinimized: boolean; @@ -211,7 +214,25 @@ export default class UserMenu extends React.Component { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO@onCommunityMembersClick"); + // 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.getGeneralChat(TagOrderStore.getSelectedPrototypeTag()); + 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) => { diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index e2d7e3f8e0..06ce8ddda8 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -27,6 +27,8 @@ import rate_limited_func from "../../../ratelimitedfunc"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import * as sdk from "../../../index"; import CallHandler from "../../../CallHandler"; +import TagOrderStore from "../../../stores/TagOrderStore"; +import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; @@ -464,10 +466,19 @@ export default createReactClass({ } } + let inviteButtonText = _t("Invite to this room"); + const communityId = TagOrderStore.getSelectedPrototypeTag(); + if (communityId) { + const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId); + if (chat && chat.roomId === this.props.roomId) { + inviteButtonText = _t("Invite to this community"); + } + } + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); inviteButton = - { _t('Invite to this room') } + { inviteButtonText } ; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c798c8eff1..d8e159244f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2121,6 +2121,7 @@ "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", + "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", diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index eec0a8aab8..1dfcbb766a 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -24,6 +24,8 @@ 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"; interface IState { // nothing of value - we use account data @@ -54,6 +56,19 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { return profile?.name || 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 { if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) { return; diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 2eb35e6dc2..6651d207a1 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -168,6 +168,7 @@ class TagOrderStore extends Store { if (!allowMultiple && newTags.length === 1) { // We're in prototype behaviour: select the general chat for the community + // XXX: This is duplicated with the CommunityPrototypeStore as a cyclical reference const rooms = GroupStore.getGroupRooms(newTags[0]) .map(r => MatrixClientPeg.get().getRoom(r.roomId)) .filter(r => !!r); From 724e3f690518a7215b1b3bbfe7f05e8c019727b4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 Aug 2020 10:19:05 -0600 Subject: [PATCH 15/22] Run all selected prototype community logic through one store --- src/components/structures/UserMenu.tsx | 4 ++-- src/components/views/dialogs/CreateRoomDialog.js | 9 ++++----- src/components/views/dialogs/InviteDialog.js | 9 ++++----- src/components/views/rooms/MemberList.js | 10 +++------- src/components/views/rooms/RoomList.tsx | 4 ++-- src/i18n/strings/en_EN.json | 2 +- src/stores/CommunityPrototypeStore.ts | 16 +++++++++++++++- src/stores/TagOrderStore.js | 7 ------- 8 files changed, 31 insertions(+), 30 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index a583af2603..baa5d661a3 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -218,7 +218,7 @@ export default class UserMenu extends React.Component { // 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.getGeneralChat(TagOrderStore.getSelectedPrototypeTag()); + const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); if (chat) { dis.dispatch({ action: 'view_room', @@ -239,7 +239,7 @@ export default class UserMenu extends React.Component { ev.preventDefault(); ev.stopPropagation(); - showCommunityInviteDialog(TagOrderStore.getSelectedPrototypeTag()); + showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId()); this.setState({contextMenuPosition: null}); // also close the menu }; diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index bdd3de07c0..5d370af341 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -25,7 +25,6 @@ import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {Key} from "../../../Keyboard"; import {privateShouldBeEncrypted} from "../../../createRoom"; -import TagOrderStore from "../../../stores/TagOrderStore"; import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; export default createReactClass({ @@ -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.", )}

; - if (TagOrderStore.getSelectedPrototypeTag()) { + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { publicPrivateLabel =

{_t( "Private rooms can be found and joined by invitation only. Public rooms can be " + "found and joined by anyone in this community.", @@ -239,7 +238,7 @@ export default createReactClass({ } let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); - if (TagOrderStore.getSelectedPrototypeTag()) { + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); title = _t("Create a room in %(communityName)s", {communityName: name}); } diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index c2fd7e5b0e..80d8f1fc2c 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -37,7 +37,6 @@ import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; import {DefaultTagID} from "../../../stores/room-list/models"; import RoomListStore from "../../../stores/room-list/RoomListStore"; -import TagOrderStore from "../../../stores/TagOrderStore"; import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. @@ -913,7 +912,7 @@ export default class InviteDialog extends React.PureComponent { _onCommunityInviteClick = (e) => { this.props.onFinished(); - showCommunityInviteDialog(TagOrderStore.getSelectedPrototypeTag()); + showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId()); }; _renderSection(kind: "recents"|"suggestions") { @@ -924,8 +923,8 @@ export default class InviteDialog extends React.PureComponent { let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions"); let sectionSubname = null; - if (kind === 'suggestions' && TagOrderStore.getSelectedPrototypeTag()) { - const communityName = CommunityPrototypeStore.instance.getCommunityName(TagOrderStore.getSelectedPrototypeTag()); + if (kind === 'suggestions' && CommunityPrototypeStore.instance.getSelectedCommunityId()) { + const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); sectionSubname = _t("May include members not in %(communityName)s", {communityName}); } @@ -1097,7 +1096,7 @@ export default class InviteDialog extends React.PureComponent { return {userId}; }}, ); - if (TagOrderStore.getSelectedPrototypeTag()) { + if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { const communityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); helpText = _t( "Start a conversation with someone using their name, username (like ) or email address. " + diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 06ce8ddda8..15b629921c 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -27,7 +27,6 @@ import rate_limited_func from "../../../ratelimitedfunc"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import * as sdk from "../../../index"; import CallHandler from "../../../CallHandler"; -import TagOrderStore from "../../../stores/TagOrderStore"; import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; const INITIAL_LOAD_NUM_MEMBERS = 30; @@ -467,12 +466,9 @@ export default createReactClass({ } let inviteButtonText = _t("Invite to this room"); - const communityId = TagOrderStore.getSelectedPrototypeTag(); - if (communityId) { - const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId); - if (chat && chat.roomId === this.props.roomId) { - inviteButtonText = _t("Invite to this community"); - } + const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); + if (chat && chat.roomId === this.props.roomId) { + inviteButtonText = _t("Invite to this community"); } const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 92c5982276..72e016d8e7 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -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: { }} /> { 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(TagOrderStore.getSelectedPrototypeTag()); + return CommunityPrototypeStore.instance.getCommunityName(this.getSelectedCommunityId()); + } + + public getSelectedCommunityGeneralChat(): Room { + const communityId = this.getSelectedCommunityId(); + if (communityId) { + return this.getGeneralChat(communityId); + } } public getCommunityName(communityId: string): string { diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 6651d207a1..3dfdc5feaf 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -286,13 +286,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) { From 133f981fa8cb4b1efd55c4263e70e9995c3f6c39 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 Aug 2020 10:22:29 -0600 Subject: [PATCH 16/22] Run the tag selection behaviour through the prototype store too --- src/stores/CommunityPrototypeStore.ts | 10 ++++++++++ src/stores/TagOrderStore.js | 20 -------------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 1bc0a46376..501ebfde17 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -26,6 +26,7 @@ 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 @@ -111,6 +112,15 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { 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, + }); + } } } diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 3dfdc5feaf..2b72a963b0 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -166,26 +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 - // XXX: This is duplicated with the CommunityPrototypeStore as a cyclical reference - 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; From fdbaddbace0495353020d2b02c81183c08148ff8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 Aug 2020 10:52:08 -0600 Subject: [PATCH 17/22] Add a simple edit dialog for communities --- res/css/_components.scss | 1 + .../_EditCommunityPrototypeDialog.scss | 77 ++++++++ src/components/structures/UserMenu.tsx | 6 +- .../dialogs/EditCommunityPrototypeDialog.tsx | 166 ++++++++++++++++++ src/i18n/strings/en_EN.json | 2 + src/stores/CommunityPrototypeStore.ts | 4 + 6 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 res/css/views/dialogs/_EditCommunityPrototypeDialog.scss create mode 100644 src/components/views/dialogs/EditCommunityPrototypeDialog.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 24d2ffa2b0..45ed6b3300 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss b/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss new file mode 100644 index 0000000000..75a56bf6b3 --- /dev/null +++ b/res/css/views/dialogs/_EditCommunityPrototypeDialog.scss @@ -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; + } + } + } +} diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index baa5d661a3..93b1e2f820 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -49,6 +49,7 @@ 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; @@ -207,7 +208,10 @@ export default class UserMenu extends React.Component { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO@onCommunitySettingsClick"); + Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, { + communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(), + }); + this.setState({contextMenuPosition: null}); // also close the menu }; private onCommunityMembersClick = (ev: ButtonEvent) => { diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx new file mode 100644 index 0000000000..66b49ea7b7 --- /dev/null +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -0,0 +1,166 @@ +/* +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 { + private avatarUploadRef: React.RefObject = 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) => { + 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) => { + 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) => { + 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 = ; + if (!this.state.avatarPreview) { + if (this.state.currentAvatarUrl) { + const url = MatrixClientPeg.get().mxcUrlToHttp(this.state.currentAvatarUrl); + preview = ; + } else { + preview =

+ } + } + + return ( + +
+
+
+ +
+
+ + + {preview} + +
+ {_t("Add image (optional)")} + + {_t("An image will help people identify your community.")} + +
+
+ + {_t("Save")} + +
+
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c43fc9d878..9265aec319 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1687,6 +1687,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.", diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 501ebfde17..db747d105c 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -71,6 +71,10 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { 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)) From 7f7414ed5afe0817f67e71d22571dadcdbf82175 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 Aug 2020 10:59:37 -0600 Subject: [PATCH 18/22] Appease the linter --- src/stores/FlairStore.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index 67d9616741..cb181c5c69 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -152,9 +152,9 @@ 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 The matrix client to use to fetch the profile, if needed. - * @param groupId The group ID to get the profile for. - * @returns The profile if known, otherwise null. + * @param matrixClient {MatrixClient} The matrix client to use to fetch the profile, if needed. + * @param groupId {string} The group ID to get the profile for. + * @returns {*} The profile if known, otherwise null. */ getGroupProfileCachedFast(matrixClient, groupId) { if (!matrixClient || !groupId) return null; From b4f62e9c8863d5a2bef077d567011ad81284ae9a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 Aug 2020 11:07:29 -0600 Subject: [PATCH 19/22] use valid jsdoc --- src/RoomInvite.js | 1 - src/stores/FlairStore.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index b82cc0a8e7..7eb7f5dbb2 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -24,7 +24,6 @@ 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 GroupStore from "./stores/GroupStore"; import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; /** diff --git a/src/stores/FlairStore.js b/src/stores/FlairStore.js index cb181c5c69..53d07d0452 100644 --- a/src/stores/FlairStore.js +++ b/src/stores/FlairStore.js @@ -152,8 +152,8 @@ 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 groupId {string} The group ID to get the profile for. + * @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) { From 78d5b87fbc816b78f6615bd004de0642179d0498 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 31 Aug 2020 11:20:28 -0600 Subject: [PATCH 20/22] Use a different border variable for compatibility with custom themes --- res/css/structures/_UserMenu.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index 8c944935ed..6fa2f2578e 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -36,7 +36,7 @@ limitations under the License. // we cheat opacity on the theme colour with an after selector here &::after { content: ''; - border-bottom: 1px solid $roomsublist-divider-color; + border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse opacity: 0.2; display: block; padding-top: 8px; @@ -154,7 +154,7 @@ limitations under the License. width: 85%; opacity: 0.2; border: none; - border-bottom: 1px solid $roomsublist-divider-color; + border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse } &.mx_IconizedContextMenu { From 9b12355b2a30831e56abc9a1bb9f6a3159dc0937 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 2 Sep 2020 08:59:24 -0600 Subject: [PATCH 21/22] Appease the linter --- .../views/dialogs/EditCommunityPrototypeDialog.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx index 66b49ea7b7..3071854b3e 100644 --- a/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/EditCommunityPrototypeDialog.tsx @@ -145,9 +145,10 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent - - {preview} - + {preview}
{_t("Add image (optional)")} From beb77799f6a19486e1621667096ce587cbe39018 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 2 Sep 2020 17:26:23 +0100 Subject: [PATCH 22/22] Respect user preference for whether pills should have an avatar or not --- src/components/views/elements/ReplyThread.js | 10 ++++++++-- src/components/views/settings/BridgeTile.js | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 409bf9e01f..d9fd456a0e 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -331,8 +331,14 @@ export default class ReplyThread extends React.Component { { _t('In reply to ', {}, { 'a': (sub) => { sub }, - 'pill': , + 'pill': ( + + ), }) } ; diff --git a/src/components/views/settings/BridgeTile.js b/src/components/views/settings/BridgeTile.js index 5b74e44c9e..e9c58518e4 100644 --- a/src/components/views/settings/BridgeTile.js +++ b/src/components/views/settings/BridgeTile.js @@ -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")} />, });