text: /*
  Copyright 2017 Vector Creations Ltd.
  Copyright 2017, 2018 New Vector Ltd.
  Copyright 2019 The Matrix.org Foundation C.I.C.
  
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
  
      http://www.apache.org/licenses/LICENSE-2.0
  
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
  */
  
  import React from 'react';
  import PropTypes from 'prop-types';
  import classnames from 'classnames';
  import { Group } from "matrix-js-sdk/src/models/group";
  import { sleep } from "matrix-js-sdk/src/utils";
  import { logger } from "matrix-js-sdk/src/logger";
  
  import { MatrixClientPeg } from '../../MatrixClientPeg';
  import * as sdk from '../../index';
  import dis from '../../dispatcher/dispatcher';
  import { getHostingLink } from '../../utils/HostingLink';
  import { sanitizedHtmlNode } from '../../HtmlUtils';
  import { _t, _td } from '../../languageHandler';
  import AccessibleButton from '../views/elements/AccessibleButton';
  import GroupHeaderButtons from '../views/right_panel/GroupHeaderButtons';
  import MainSplit from './MainSplit';
  import RightPanel from './RightPanel';
  import Modal from '../../Modal';
  import GroupStore from '../../stores/GroupStore';
  import FlairStore from '../../stores/FlairStore';
  import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
  import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks";
  import RightPanelStore from "../../stores/right-panel/RightPanelStore";
  import AutoHideScrollbar from "./AutoHideScrollbar";
  import { mediaFromMxc } from "../../customisations/Media";
  import { replaceableComponent } from "../../utils/replaceableComponent";
  import { createSpaceFromCommunity } from "../../utils/space";
  import { Action } from "../../dispatcher/actions";
  import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
  import { UPDATE_EVENT } from "../../stores/AsyncStore";
  
  const LONG_DESC_PLACEHOLDER = _td(
      `<h1>HTML for your community's page</h1>
  <p>
      Use the long description to introduce new members to the community, or distribute
      some important <a href="foo">links</a>
  </p>
  <p>
      You can even add images with Matrix URLs <img src="mxc://url" />
  </p>
  `);
  
  const RoomSummaryType = PropTypes.shape({
      room_id: PropTypes.string.isRequired,
      profile: PropTypes.shape({
          name: PropTypes.string,
          avatar_url: PropTypes.string,
          canonical_alias: PropTypes.string,
      }).isRequired,
  });
  
  const UserSummaryType = PropTypes.shape({
      summaryInfo: PropTypes.shape({
          user_id: PropTypes.string.isRequired,
          role_id: PropTypes.string,
          avatar_url: PropTypes.string,
          displayname: PropTypes.string,
      }).isRequired,
  });
  
  class CategoryRoomList extends React.Component {
      static propTypes = {
          rooms: PropTypes.arrayOf(RoomSummaryType).isRequired,
          category: PropTypes.shape({
              profile: PropTypes.shape({
                  name: PropTypes.string,
              }).isRequired,
          }),
          groupId: PropTypes.string.isRequired,
  
          // Whether the list should be editable
          editing: PropTypes.bool.isRequired,
      };
  
      onAddRoomsToSummaryClicked = (ev) => {
          ev.preventDefault();
          const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
          Modal.createTrackedDialog('Add Rooms to Group Summary', '', AddressPickerDialog, {
              title: _t('Add rooms to the community summary'),
              description: _t("Which rooms would you like to add to this summary?"),
              placeholder: _t("Room name or address"),
              button: _t("Add to summary"),
              pickerType: 'room',
              validAddressTypes: ['mx-room-id'],
              groupId: this.props.groupId,
              onFinished: (success, addrs) => {
                  if (!success) return;
                  const errorList = [];
                  Promise.allSettled(addrs.map((addr) => {
                      return GroupStore
                          .addRoomToGroupSummary(this.props.groupId, addr.address)
                          .catch(() => { errorList.push(addr.address); });
                  })).then(() => {
                      if (errorList.length === 0) {
                          return;
                      }
                      const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                      Modal.createTrackedDialog(
                          'Failed to add the following room to the group summary',
                          '',
                          ErrorDialog,
                          {
                              title: _t(
                                  "Failed to add the following rooms to the summary of %(groupId)s:",
                                  { groupId: this.props.groupId },
                              ),
                              description: errorList.join(", "),
                          },
                      );
                  });
              },
          }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
      };
  
      render() {
          const addButton = this.props.editing ?
              (<AccessibleButton className="mx_GroupView_featuredThings_addButton"
                  onClick={this.onAddRoomsToSummaryClicked}
              >
                  <img src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
                  <div className="mx_GroupView_featuredThings_addButton_label">
                      { _t('Add a Room') }
                  </div>
              </AccessibleButton>) : <div />;
  
          const roomNodes = this.props.rooms.map((r) => {
              return <FeaturedRoom
                  key={r.room_id}
                  groupId={this.props.groupId}
                  editing={this.props.editing}
                  summaryInfo={r} />;
          });
  
          let catHeader = <div />;
          if (this.props.category && this.props.category.profile) {
              catHeader = <div className="mx_GroupView_featuredThings_category">
                  { this.props.category.profile.name }
              </div>;
          }
          return <div className="mx_GroupView_featuredThings_container">
              { catHeader }
              { roomNodes }
              { addButton }
          </div>;
      }
  }
  
  class FeaturedRoom extends React.Component {
      static propTypes = {
          summaryInfo: RoomSummaryType.isRequired,
          editing: PropTypes.bool.isRequired,
          groupId: PropTypes.string.isRequired,
      };
  
      onClick = (e) => {
          e.preventDefault();
          e.stopPropagation();
  
          dis.dispatch({
              action: Action.ViewRoom,
              room_alias: this.props.summaryInfo.profile.canonical_alias,
              room_id: this.props.summaryInfo.room_id,
          });
      };
  
      onDeleteClicked = (e) => {
          e.preventDefault();
          e.stopPropagation();
          GroupStore.removeRoomFromGroupSummary(
              this.props.groupId,
              this.props.summaryInfo.room_id,
          ).catch((err) => {
              logger.error('Error whilst removing room from group summary', err);
              const roomName = this.props.summaryInfo.name ||
                  this.props.summaryInfo.canonical_alias ||
                  this.props.summaryInfo.room_id;
              const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
              Modal.createTrackedDialog(
                  'Failed to remove room from group summary',
                  '', ErrorDialog,
                  {
                      title: _t(
                          "Failed to remove the room from the summary of %(groupId)s",
                          { groupId: this.props.groupId },
                      ),
                      description: _t("The room '%(roomName)s' could not be removed from the summary.", { roomName }),
                  },
              );
          });
      };
  
      render() {
          const RoomAvatar = sdk.getComponent("avatars.RoomAvatar");
  
          const roomName = this.props.summaryInfo.profile.name ||
              this.props.summaryInfo.profile.canonical_alias ||
              _t("Unnamed Room");
  
          const oobData = {
              roomId: this.props.summaryInfo.room_id,
              avatarUrl: this.props.summaryInfo.profile.avatar_url,
              name: roomName,
          };
  
          let permalink = null;
          if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
              permalink = makeGroupPermalink(this.props.summaryInfo.profile.canonical_alias);
          }
  
          let roomNameNode = null;
          if (permalink) {
              roomNameNode = <a href={permalink} onClick={this.onClick}>{ roomName }</a>;
          } else {
              roomNameNode = <span>{ roomName }</span>;
          }
  
          const deleteButton = this.props.editing ?
              <img
                  className="mx_GroupView_featuredThing_deleteButton"
                  src={require("../../../res/img/cancel-small.svg")}
                  width="14"
                  height="14"
                  alt="Delete"
                  onClick={this.onDeleteClicked} />
              : <div />;
  
          return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
              <RoomAvatar oobData={oobData} width={64} height={64} />
              <div className="mx_GroupView_featuredThing_name">{ roomNameNode }</div>
              { deleteButton }
          </AccessibleButton>;
      }
  }
  
  class RoleUserList extends React.Component {
      static propTypes = {
          users: PropTypes.arrayOf(UserSummaryType).isRequired,
          role: PropTypes.shape({
              profile: PropTypes.shape({
                  name: PropTypes.string,
              }).isRequired,
          }),
          groupId: PropTypes.string.isRequired,
  
          // Whether the list should be editable
          editing: PropTypes.bool.isRequired,
      };
  
      onAddUsersClicked = (ev) => {
          ev.preventDefault();
          const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
          Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, {
              title: _t('Add users to the community summary'),
              description: _t("Who would you like to add to this summary?"),
              placeholder: _t("Name or Matrix ID"),
              button: _t("Add to summary"),
              validAddressTypes: ['mx-user-id'],
              groupId: this.props.groupId,
              shouldOmitSelf: false,
              onFinished: (success, addrs) => {
                  if (!success) return;
                  const errorList = [];
                  Promise.allSettled(addrs.map((addr) => {
                      return GroupStore
                          .addUserToGroupSummary(addr.address)
                          .catch(() => { errorList.push(addr.address); });
                  })).then(() => {
                      if (errorList.length === 0) {
                          return;
                      }
                      const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                      Modal.createTrackedDialog(
                          'Failed to add the following users to the community summary',
                          '', ErrorDialog,
                          {
                              title: _t(
                                  "Failed to add the following users to the summary of %(groupId)s:",
                                  { groupId: this.props.groupId },
                              ),
                              description: errorList.join(", "),
                          },
                      );
                  });
              },
          }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
      };
  
      render() {
          const addButton = this.props.editing ?
              (<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
                  <img src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
                  <div className="mx_GroupView_featuredThings_addButton_label">
                      { _t('Add a User') }
                  </div>
              </AccessibleButton>) : <div />;
          const userNodes = this.props.users.map((u) => {
              return <FeaturedUser
                  key={u.user_id}
                  summaryInfo={u}
                  editing={this.props.editing}
                  groupId={this.props.groupId} />;
          });
          let roleHeader = <div />;
          if (this.props.role && this.props.role.profile) {
              roleHeader = <div className="mx_GroupView_featuredThings_category">{ this.props.role.profile.name }</div>;
          }
          return <div className="mx_GroupView_featuredThings_container">
              { roleHeader }
              { userNodes }
              { addButton }
          </div>;
      }
  }
  
  class FeaturedUser extends React.Component {
      static propTypes = {
          summaryInfo: UserSummaryType.isRequired,
          editing: PropTypes.bool.isRequired,
          groupId: PropTypes.string.isRequired,
      };
  
      onClick = (e) => {
          e.preventDefault();
          e.stopPropagation();
  
          dis.dispatch({
              action: Action.ViewStartChatOrReuse,
              user_id: this.props.summaryInfo.user_id,
          });
      };
  
      onDeleteClicked = (e) => {
          e.preventDefault();
          e.stopPropagation();
          GroupStore.removeUserFromGroupSummary(
              this.props.groupId,
              this.props.summaryInfo.user_id,
          ).catch((err) => {
              logger.error('Error whilst removing user from group summary', err);
              const displayName = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
              const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
              Modal.createTrackedDialog(
                  'Failed to remove user from community summary',
                  '',
                  ErrorDialog,
                  {
                      title: _t(
                          "Failed to remove a user from the summary of %(groupId)s",
                          { groupId: this.props.groupId },
                      ),
                      description: _t(
                          "The user '%(displayName)s' could not be removed from the summary.",
                          { displayName },
                      ),
                  },
              );
          });
      };
  
      render() {
          const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
          const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
  
          const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
          const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
          const httpUrl = mediaFromMxc(this.props.summaryInfo.avatar_url).getSquareThumbnailHttp(64);
  
          const deleteButton = this.props.editing ?
              <img
                  className="mx_GroupView_featuredThing_deleteButton"
                  src={require("../../../res/img/cancel-small.svg")}
                  width="14"
                  height="14"
                  alt="Delete"
                  onClick={this.onDeleteClicked} />
              : <div />;
  
          return <AccessibleButton className="mx_GroupView_featuredThing" onClick={this.onClick}>
              <BaseAvatar name={name} url={httpUrl} width={64} height={64} />
              <div className="mx_GroupView_featuredThing_name">{ userNameNode }</div>
              { deleteButton }
          </AccessibleButton>;
      }
  }
  
  const GROUP_JOINPOLICY_OPEN = "open";
  const GROUP_JOINPOLICY_INVITE = "invite";
  
  const UPGRADE_NOTICE_LS_KEY = "mx_hide_community_upgrade_notice";
  
  @replaceableComponent("structures.GroupView")
  export default class GroupView extends React.Component {
      static propTypes = {
          groupId: PropTypes.string.isRequired,
          // Whether this is the first time the group admin is viewing the group
          groupIsNew: PropTypes.bool,
      };
  
      state = {
          summary: null,
          isGroupPublicised: null,
          isUserPrivileged: null,
          groupRooms: null,
          groupRoomsLoading: null,
          error: null,
          editing: false,
          saving: false,
          uploadingAvatar: false,
          avatarChanged: false,
          membershipBusy: false,
          publicityBusy: false,
          inviterProfile: null,
          showRightPanel: RightPanelStore.instance.isOpenForGroup,
          showUpgradeNotice: !localStorage.getItem(UPGRADE_NOTICE_LS_KEY),
      };
  
      componentDidMount() {
          this._unmounted = false;
          this._matrixClient = MatrixClientPeg.get();
          this._matrixClient.on("Group.myMembership", this._onGroupMyMembership);
  
          this._initGroupStore(this.props.groupId, true);
  
          this._dispatcherRef = dis.register(this._onAction);
          RightPanelStore.instance.on(UPDATE_EVENT, this._onRightPanelStoreUpdate);
      }
  
      componentWillUnmount() {
          this._unmounted = true;
          this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
          dis.unregister(this._dispatcherRef);
  
          RightPanelStore.instance.off(UPDATE_EVENT, this._onRightPanelStoreUpdate);
      }
  
      // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
      // eslint-disable-next-line camelcase
      UNSAFE_componentWillReceiveProps(newProps) {
          if (this.props.groupId !== newProps.groupId) {
              this.setState({
                  summary: null,
                  error: null,
              }, () => {
                  this._initGroupStore(newProps.groupId);
              });
          }
      }
  
      _onRightPanelStoreUpdate = () => {
          this.setState({
              showRightPanel: RightPanelStore.instance.isOpenForGroup,
          });
      };
  
      _onGroupMyMembership = (group) => {
          if (this._unmounted || group.groupId !== this.props.groupId) return;
          if (group.myMembership === 'leave') {
              // Leave settings - the user might have clicked the "Leave" button
              this._closeSettings();
          }
          this.setState({ membershipBusy: false });
      };
  
      _initGroupStore(groupId, firstInit) {
          const group = this._matrixClient.getGroup(groupId);
          if (group && group.inviter && group.inviter.userId) {
              this._fetchInviterProfile(group.inviter.userId);
          }
          GroupStore.registerListener(groupId, this.onGroupStoreUpdated.bind(this, firstInit));
          let willDoOnboarding = false;
          // XXX: This should be more fluxy - let's get the error from GroupStore .getError or something
          GroupStore.on('error', (err, errorGroupId, stateKey) => {
              if (this._unmounted || groupId !== errorGroupId) return;
              if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN' && !willDoOnboarding) {
                  dis.dispatch({
                      action: Action.DoAfterSyncPrepared,
                      deferred_action: {
                          action: 'view_group',
                          group_id: groupId,
                      },
                  });
                  dis.dispatch({ action: 'require_registration', screen_after: { screen: `group/${groupId}` } });
                  willDoOnboarding = true;
              }
              if (stateKey === GroupStore.STATE_KEY.Summary) {
                  this.setState({
                      summary: null,
                      error: err,
                      editing: false,
                  });
              }
          });
      }
  
      onGroupStoreUpdated = (firstInit) => {
          if (this._unmounted) return;
          const summary = GroupStore.getSummary(this.props.groupId);
          if (summary.profile) {
              // Default profile fields should be "" for later sending to the server (which
              // requires that the fields are strings, not null)
              ["avatar_url", "long_description", "name", "short_description"].forEach((k) => {
                  summary.profile[k] = summary.profile[k] || "";
              });
          }
          this.setState({
              summary,
              summaryLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.Summary),
              isGroupPublicised: GroupStore.getGroupPublicity(this.props.groupId),
              isUserPrivileged: GroupStore.isUserPrivileged(this.props.groupId),
              groupRooms: GroupStore.getGroupRooms(this.props.groupId),
              groupRoomsLoading: !GroupStore.isStateReady(this.props.groupId, GroupStore.STATE_KEY.GroupRooms),
              isUserMember: GroupStore.getGroupMembers(this.props.groupId).some(
                  (m) => m.userId === this._matrixClient.credentials.userId,
              ),
          });
          // XXX: This might not work but this.props.groupIsNew unused anyway
          if (this.props.groupIsNew && firstInit) {
              this._onEditClick();
          }
      };
  
      _fetchInviterProfile(userId) {
          this.setState({
              inviterProfileBusy: true,
          });
          this._matrixClient.getProfileInfo(userId).then((resp) => {
              if (this._unmounted) return;
              this.setState({
                  inviterProfile: {
                      avatarUrl: resp.avatar_url,
                      displayName: resp.displayname,
                  },
              });
          }).catch((e) => {
              logger.error('Error getting group inviter profile', e);
          }).finally(() => {
              if (this._unmounted) return;
              this.setState({
                  inviterProfileBusy: false,
              });
          });
      }
  
      _onEditClick = () => {
          this.setState({
              editing: true,
              profileForm: Object.assign({}, this.state.summary.profile),
              joinableForm: {
                  policyType:
                      this.state.summary.profile.is_openly_joinable ?
                          GROUP_JOINPOLICY_OPEN :
                          GROUP_JOINPOLICY_INVITE,
              },
          });
      };
  
      _onShareClick = () => {
          const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
          Modal.createTrackedDialog('share community dialog', '', ShareDialog, {
              target: this._matrixClient.getGroup(this.props.groupId) || new Group(this.props.groupId),
          });
      };
  
      _onCancelClick = () => {
          this._closeSettings();
      };
  
      _onAction = (payload) => {
          switch (payload.action) {
              // NOTE: close_settings is an app-wide dispatch; as it is dispatched from MatrixChat
              case 'close_settings':
                  this.setState({
                      editing: false,
                      profileForm: null,
                  });
                  break;
              default:
                  break;
          }
      };
  
      _closeSettings = () => {
          dis.dispatch({ action: 'close_settings' });
      };
  
      _onNameChange = (value) => {
          const newProfileForm = Object.assign(this.state.profileForm, { name: value });
          this.setState({
              profileForm: newProfileForm,
          });
      };
  
      _onShortDescChange = (value) => {
          const newProfileForm = Object.assign(this.state.profileForm, { short_description: value });
          this.setState({
              profileForm: newProfileForm,
          });
      };
  
      _onLongDescChange = (e) => {
          const newProfileForm = Object.assign(this.state.profileForm, { long_description: e.target.value });
          this.setState({
              profileForm: newProfileForm,
          });
      };
  
      _onAvatarSelected = ev => {
          const file = ev.target.files[0];
          if (!file) return;
  
          this.setState({ uploadingAvatar: true });
          this._matrixClient.uploadContent(file).then((url) => {
              const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url });
              this.setState({
                  uploadingAvatar: false,
                  profileForm: newProfileForm,
  
                  // Indicate that FlairStore needs to be poked to show this change
                  // in TagTile (GroupFilterPanel), Flair and GroupTile (MyGroups).
                  avatarChanged: true,
              });
          }).catch((e) => {
              this.setState({ uploadingAvatar: false });
              const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
              logger.error("Failed to upload avatar image", e);
              Modal.createTrackedDialog('Failed to upload image', '', ErrorDialog, {
                  title: _t('Error'),
                  description: _t('Failed to upload image'),
              });
          });
      };
  
      _onJoinableChange = ev => {
          this.setState({
              joinableForm: { policyType: ev.target.value },
          });
      };
  
      _onSaveClick = () => {
          this.setState({ saving: true });
          const savePromise = this.state.isUserPrivileged ? this._saveGroup() : Promise.resolve();
          savePromise.then((result) => {
              this.setState({
                  saving: false,
                  editing: false,
                  summary: null,
              });
              this._initGroupStore(this.props.groupId);
  
              if (this.state.avatarChanged) {
                  // XXX: Evil - poking a store should be done from an async action
                  FlairStore.refreshGroupProfile(this._matrixClient, this.props.groupId);
              }
          }).catch((e) => {
              this.setState({
                  saving: false,
              });
              const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
              logger.error("Failed to save community profile", e);
              Modal.createTrackedDialog('Failed to update group', '', ErrorDialog, {
                  title: _t('Error'),
                  description: _t('Failed to update community'),
              });
          }).finally(() => {
              this.setState({
                  avatarChanged: false,
              });
          });
      };
  
      async _saveGroup() {
          await this._matrixClient.setGroupProfile(this.props.groupId, this.state.profileForm);
          await this._matrixClient.setGroupJoinPolicy(this.props.groupId, {
              type: this.state.joinableForm.policyType,
          });
      }
  
      _onAcceptInviteClick = async () => {
          this.setState({ membershipBusy: true });
  
          // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
          // spinner disappearing after we have fetched new group data.
          await sleep(500);
  
          GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
              // don't reset membershipBusy here: wait for the membership change to come down the sync
          }).catch((e) => {
              this.setState({ membershipBusy: false });
              const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
              Modal.createTrackedDialog('Error accepting invite', '', ErrorDialog, {
                  title: _t("Error"),
                  description: _t("Unable to accept invite"),
              });
          });
      };
  
      _onRejectInviteClick = async () => {
          this.setState({ membershipBusy: true });
  
          // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
          // spinner disappearing after we have fetched new group data.
          await sleep(500);
  
          GroupStore.leaveGroup(this.props.groupId).then(() => {
              // don't reset membershipBusy here: wait for the membership change to come down the sync
          }).catch((e) => {
              this.setState({ membershipBusy: false });
              const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
              Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
                  title: _t("Error"),
                  description: _t("Unable to reject invite"),
              });
          });
      };
  
      _onJoinClick = async () => {
          if (this._matrixClient.isGuest()) {
              dis.dispatch({ action: 'require_registration', screen_after: { screen: `group/${this.props.groupId}` } });
              return;
          }
  
          this.setState({ membershipBusy: true });
  
          // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
          // spinner disappearing after we have fetched new group data.
          await sleep(500);
  
          GroupStore.joinGroup(this.props.groupId).then(() => {
              // don't reset membershipBusy here: wait for the membership change to come down the sync
          }).catch((e) => {
              this.setState({ membershipBusy: false });
              const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
              Modal.createTrackedDialog('Error joining room', '', ErrorDialog, {
                  title: _t("Error"),
                  description: _t("Unable to join community"),
              });
          });
      };
  
      _leaveGroupWarnings() {
          const warnings = [];
  
          if (this.state.isUserPrivileged) {
              warnings.push((
                  <span className="warning">
                      { " " /* Whitespace, otherwise the sentences get smashed together */ }
                      { _t("You are an administrator of this community. You will not be " +
                           "able to rejoin without an invite from another administrator.") }
                  </span>
              ));
          }
  
          return warnings;
      }
  
      _onLeaveClick = () => {
          const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
          const warnings = this._leaveGroupWarnings();
  
          Modal.createTrackedDialog('Leave Group', '', QuestionDialog, {
              title: _t("Leave Community"),
              description: (
                  <span>
                      { _t("Leave %(groupName)s?", { groupName: this.props.groupId }) }
                      { warnings }
                  </span>
              ),
              button: _t("Leave"),
              danger: this.state.isUserPrivileged,
              onFinished: async (confirmed) => {
                  if (!confirmed) return;
  
                  this.setState({ membershipBusy: true });
  
                  // Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
                  // spinner disappearing after we have fetched new group data.
                  await sleep(500);
  
                  GroupStore.leaveGroup(this.props.groupId).then(() => {
                      // don't reset membershipBusy here: wait for the membership change to come down the sync
                  }).catch((e) => {
                      this.setState({ membershipBusy: false });
                      const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
                      Modal.createTrackedDialog('Error leaving community', '', ErrorDialog, {
                          title: _t("Error"),
                          description: _t("Unable to leave community"),
                      });
                  });
              },
          });
      };
  
      _onAddRoomsClick = () => {
          showGroupAddRoomDialog(this.props.groupId);
      };
  
      _dismissUpgradeNotice = () => {
          localStorage.setItem(UPGRADE_NOTICE_LS_KEY, "true");
          this.setState({ showUpgradeNotice: false });
      };
  
      _onCreateSpaceClick = () => {
          createSpaceFromCommunity(this._matrixClient, this.props.groupId);
      };
  
      _onAdminsLinkClick = () => {
          RightPanelStore.instance.setCard({ phase: RightPanelPhases.GroupMemberList });
      };
  
      _getGroupSection() {
          const groupSettingsSectionClasses = classnames({
              "mx_GroupView_group": this.state.editing,
              "mx_GroupView_group_disabled": this.state.editing && !this.state.isUserPrivileged,
          });
  
          const header = this.state.editing ? <h2> { _t('Community Settings') } </h2> : <div />;
  
          const hostingSignupLink = getHostingLink('community-settings');
          let hostingSignup = null;
          if (hostingSignupLink && this.state.isUserPrivileged) {
              hostingSignup = <div className="mx_GroupView_hostingSignup">
                  { _t(
                      "Want more than a community? <a>Get your own server</a>", {},
                      {
                          a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
                      },
                  ) }
                  <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
                      <img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
                  </a>
              </div>;
          }
  
          const changeDelayWarning = this.state.editing && this.state.isUserPrivileged ?
              <div className="mx_GroupView_changeDelayWarning">
                  { _t(
                      'Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> ' +
                      'might not be seen by other users for up to 30 minutes.',
                      {},
                      {
                          'bold1': (sub) => <b> { sub } </b>,
                          'bold2': (sub) => <b> { sub } </b>,
                      },
                  ) }
              </div> : <div />;
  
          let communitiesUpgradeNotice;
          if (this.state.showUpgradeNotice) {
              let text;
              if (this.state.isUserPrivileged) {
                  text = _t("You can create a Space from this community <a>here</a>.", {}, {
                      a: sub => <AccessibleButton onClick={this._onCreateSpaceClick} kind="link">
                          { sub }
                      </AccessibleButton>,
                  });
              } else {
                  text = _t("Ask the <a>admins</a> of this community to make it into a Space " +
                      "and keep a look out for the invite.", {}, {
                      a: sub => <AccessibleButton onClick={this._onAdminsLinkClick} kind="link">
                          { sub }
                      </AccessibleButton>,
                  });
              }
  
              communitiesUpgradeNotice = <div className="mx_GroupView_spaceUpgradePrompt">
                  <h2>{ _t("Communities can now be made into Spaces") }</h2>
                  <p>
                      { _t("Spaces are a new way to make a community, with new features coming.") }
                      &nbsp;
                      { text }
                      &nbsp;
                      { _t("Communities won't receive further updates.") }
                  </p>
                  <AccessibleButton
                      className="mx_GroupView_spaceUpgradePrompt_close"
                      onClick={this._dismissUpgradeNotice}
                  />
              </div>;
          }
  
          return <div className={groupSettingsSectionClasses}>
              { header }
              { hostingSignup }
              { changeDelayWarning }
              { communitiesUpgradeNotice }
              { this._getJoinableNode() }
              { this._getLongDescriptionNode() }
              { this._getRoomsNode() }
          </div>;
      }
  
      _getRoomsNode() {
          const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
          const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
          const Spinner = sdk.getComponent('elements.Spinner');
          const TooltipButton = sdk.getComponent('elements.TooltipButton');
  
          const roomsHelpNode = this.state.editing ? <TooltipButton helpText={
              _t(
                  'These rooms are displayed to community members on the community page. '+
                  'Community members can join the rooms by clicking on them.',
              )
          } /> : <div />;
  
          const addRoomRow = this.state.editing ?
              (<AccessibleButton className="mx_GroupView_rooms_header_addRow"
                  onClick={this._onAddRoomsClick}
              >
                  <div className="mx_GroupView_rooms_header_addRow_button">
                      <img src={require("../../../res/img/icons-room-add.svg")} width="24" height="24" />
                  </div>
                  <div className="mx_GroupView_rooms_header_addRow_label">
                      { _t('Add rooms to this community') }
                  </div>
              </AccessibleButton>) : <div />;
  
          return <div className="mx_GroupView_rooms">
              <div className="mx_GroupView_rooms_header">
                  <h3>
                      { _t('Rooms') }
                      { roomsHelpNode }
                  </h3>
                  { addRoomRow }
              </div>
              { this.state.groupRoomsLoading ?
                  <Spinner /> :
                  <RoomDetailList rooms={this.state.groupRooms} />
              }
          </div>;
      }
  
      _getFeaturedRoomsNode() {
          const summary = this.state.summary;
  
          const defaultCategoryRooms = [];
          const categoryRooms = {};
          summary.rooms_section.rooms.forEach((r) => {
              if (r.category_id === null) {
                  defaultCategoryRooms.push(r);
              } else {
                  let list = categoryRooms[r.category_id];
                  if (list === undefined) {
                      list = [];
                      categoryRooms[r.category_id] = list;
                  }
                  list.push(r);
              }
          });
  
          const defaultCategoryNode = <CategoryRoomList
              rooms={defaultCategoryRooms}
              groupId={this.props.groupId}
              editing={this.state.editing} />;
          const categoryRoomNodes = Object.keys(categoryRooms).map((catId) => {
              const cat = summary.rooms_section.categories[catId];
              return <CategoryRoomList
                  key={catId}
                  rooms={categoryRooms[catId]}
                  category={cat}
                  groupId={this.props.groupId}
                  editing={this.state.editing} />;
          });
  
          return <div className="mx_GroupView_featuredThings">
              <div className="mx_GroupView_featuredThings_header">
                  { _t('Featured Rooms:') }
              </div>
              { defaultCategoryNode }
              { categoryRoomNodes }
          </div>;
      }
  
      _getFeaturedUsersNode() {
          const summary = this.state.summary;
  
          const noRoleUsers = [];
          const roleUsers = {};
          summary.users_section.users.forEach((u) => {
              if (u.role_id === null) {
                  noRoleUsers.push(u);
              } else {
                  let list = roleUsers[u.role_id];
                  if (list === undefined) {
                      list = [];
                      roleUsers[u.role_id] = list;
                  }
                  list.push(u);
              }
          });
  
          const noRoleNode = <RoleUserList
              users={noRoleUsers}
              groupId={this.props.groupId}
              editing={this.state.editing} />;
          const roleUserNodes = Object.keys(roleUsers).map((roleId) => {
              const role = summary.users_section.roles[roleId];
              return <RoleUserList
                  key={roleId}
                  users={roleUsers[roleId]}
                  role={role}
                  groupId={this.props.groupId}
                  editing={this.state.editing} />;
          });
  
          return <div className="mx_GroupView_featuredThings">
              <div className="mx_GroupView_featuredThings_header">
                  { _t('Featured Users:') }
              </div>
              { noRoleNode }
              { roleUserNodes }
          </div>;
      }
  
      _getMembershipSection() {
          const Spinner = sdk.getComponent("elements.Spinner");
          const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
  
          const group = this._matrixClient.getGroup(this.props.groupId);
  
          if (group && group.myMembership === 'invite') {
              if (this.state.membershipBusy || this.state.inviterProfileBusy) {
                  return <div className="mx_GroupView_membershipSection">
                      <Spinner />
                  </div>;
              }
              const httpInviterAvatar = this.state.inviterProfile && this.state.inviterProfile.avatarUrl
                  ? mediaFromMxc(this.state.inviterProfile.avatarUrl).getSquareThumbnailHttp(36)
                  : null;
  
              const inviter = group.inviter || {};
              let inviterName = inviter.userId;
              if (this.state.inviterProfile) {
                  inviterName = this.state.inviterProfile.displayName || inviter.userId;
              }
              return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_invited">
                  <div className="mx_GroupView_membershipSubSection">
                      <div className="mx_GroupView_membershipSection_description">
                          <BaseAvatar url={httpInviterAvatar}
                              name={inviterName}
                              width={36}
                              height={36}
                          />
                          { _t("%(inviter)s has invited you to join this community", {
                              inviter: inviterName || _t("Someone"),
                          }) }
                      </div>
                      <div className="mx_GroupView_membership_buttonContainer">
                          <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
                              onClick={this._onAcceptInviteClick}
                          >
                              { _t("Accept") }
                          </AccessibleButton>
                          <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
                              onClick={this._onRejectInviteClick}
                          >
                              { _t("Decline") }
                          </AccessibleButton>
                      </div>
                  </div>
              </div>;
          }
  
          let membershipContainerExtraClasses;
          let membershipButtonExtraClasses;
          let membershipButtonTooltip;
          let membershipButtonText;
          let membershipButtonOnClick;
  
          // User is not in the group
          if ((!group || group.myMembership === 'leave') &&
              this.state.summary &&
              this.state.summary.profile &&
              Boolean(this.state.summary.profile.is_openly_joinable)
          ) {
              membershipButtonText = _t("Join this community");
              membershipButtonOnClick = this._onJoinClick;
  
              membershipButtonExtraClasses = 'mx_GroupView_joinButton';
              membershipContainerExtraClasses = 'mx_GroupView_membershipSection_leave';
          } else if (
              group &&
              group.myMembership === 'join' &&
              this.state.editing
          ) {
              membershipButtonText = _t("Leave this community");
              membershipButtonOnClick = this._onLeaveClick;
              membershipButtonTooltip = this.state.isUserPrivileged ?
                  _t("You are an administrator of this community") :
                  _t("You are a member of this community");
  
              membershipButtonExtraClasses = {
                  'mx_GroupView_leaveButton': true,
                  'mx_RoomHeader_textButton_danger': this.state.isUserPrivileged,
              };
              membershipContainerExtraClasses = 'mx_GroupView_membershipSection_joined';
          } else {
              return null;
          }
  
          const membershipButtonClasses = classnames(
              [
                  'mx_RoomHeader_textButton',
                  'mx_GroupView_textButton',
              ],
              membershipButtonExtraClasses,
          );
  
          const membershipContainerClasses = classnames(
              'mx_GroupView_membershipSection',
              membershipContainerExtraClasses,
          );
  
          return <div className={membershipContainerClasses}>
              <div className="mx_GroupView_membershipSubSection">
                  { /* The <div /> is for flex alignment */ }
                  { this.state.membershipBusy ? <Spinner /> : <div /> }
                  <div className="mx_GroupView_membership_buttonContainer">
                      <AccessibleButton
                          className={membershipButtonClasses}
                          onClick={membershipButtonOnClick}
                          title={membershipButtonTooltip}
                      >
                          { membershipButtonText }
                      </AccessibleButton>
                  </div>
              </div>
          </div>;
      }
  
      _getJoinableNode() {
          const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
          return this.state.editing ? <div>
              <h3>
                  { _t('Who can join this community?') }
                  { this.state.groupJoinableLoading ?
                      <InlineSpinner /> : <div />
                  }
              </h3>
              <div>
                  <label>
                      <input type="radio"
                          value={GROUP_JOINPOLICY_INVITE}
                          checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE}
                          onChange={this._onJoinableChange}
                      />
                      <div className="mx_GroupView_label_text">
                          { _t('Only people who have been invited') }
                      </div>
                  </label>
              </div>
              <div>
                  <label>
                      <input type="radio"
                          value={GROUP_JOINPOLICY_OPEN}
                          checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN}
                          onChange={this._onJoinableChange}
                      />
                      <div className="mx_GroupView_label_text">
                          { _t('Everyone') }
                      </div>
                  </label>
              </div>
          </div> : null;
      }
  
      _getLongDescriptionNode() {
          const summary = this.state.summary;
          let description = null;
          if (summary.profile && summary.profile.long_description) {
              description = sanitizedHtmlNode(summary.profile.long_description);
          } else if (this.state.isUserPrivileged) {
              description = <div
                  className="mx_GroupView_groupDesc_placeholder"
                  onClick={this._onEditClick}
              >
                  { _t(
                      'Your community hasn\'t got a Long Description, a HTML page to show to community members.<br />' +
                      'Click here to open settings and give it one!',
                      {},
                      { 'br': <br /> },
                  ) }
              </div>;
          }
          const groupDescEditingClasses = classnames({
              "mx_GroupView_groupDesc": true,
              "mx_GroupView_groupDesc_disabled": !this.state.isUserPrivileged,
          });
  
          return this.state.editing ?
              <div className={groupDescEditingClasses}>
                  <h3> { _t("Long Description (HTML)") } </h3>
                  <textarea
                      value={this.state.profileForm.long_description}
                      placeholder={_t(LONG_DESC_PLACEHOLDER)}
                      onChange={this._onLongDescChange}
                      tabIndex="4"
                      key="editLongDesc"
                  />
              </div> :
              <div className="mx_GroupView_groupDesc">
                  { description }
              </div>;
      }
  
      render() {
          const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
          const Spinner = sdk.getComponent("elements.Spinner");
  
          if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
              return <Spinner />;
          } else if (this.state.summary && !this.state.error) {
              const summary = this.state.summary;
  
              let avatarNode;
              let nameNode;
              let shortDescNode;
              const rightButtons = [];
              if (this.state.editing && this.state.isUserPrivileged) {
                  let avatarImage;
                  if (this.state.uploadingAvatar) {
                      avatarImage = <Spinner />;
                  } else {
                      const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
                      avatarImage = <GroupAvatar
                          groupId={this.props.groupId}
                          groupName={this.state.profileForm.name}
                          groupAvatarUrl={this.state.profileForm.avatar_url}
                          width={28}
                          height={28}
                          resizeMethod='crop'
                      />;
                  }
  
                  avatarNode = (
                      <div className="mx_GroupView_avatarPicker">
                          <label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
                              { avatarImage }
                          </label>
                          <div className="mx_GroupView_avatarPicker_edit">
                              <label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
                                  <img
                                      src={require("../../../res/img/camera.svg")}
                                      alt={_t("Upload avatar")}
                                      title={_t("Upload avatar")}
                                      width="17"
                                      height="15" />
                              </label>
                              <input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
                          </div>
                      </div>
                  );
  
                  const EditableText = sdk.getComponent("elements.EditableText");
  
                  nameNode = <EditableText
                      className="mx_GroupView_editable"
                      placeholderClassName="mx_GroupView_placeholder"
                      placeholder={_t('Community Name')}
                      blurToCancel={false}
                      initialValue={this.state.profileForm.name}
                      onValueChanged={this._onNameChange}
                      tabIndex="0"
                      dir="auto" />;
  
                  shortDescNode = <EditableText
                      className="mx_GroupView_editable"
                      placeholderClassName="mx_GroupView_placeholder"
                      placeholder={_t("Description")}
                      blurToCancel={false}
                      initialValue={this.state.profileForm.short_description}
                      onValueChanged={this._onShortDescChange}
                      tabIndex="0"
                      dir="auto" />;
              } else {
                  const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null;
                  const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null;
                  const groupName = summary.profile ? summary.profile.name : null;
                  avatarNode = <GroupAvatar
                      groupId={this.props.groupId}
                      groupAvatarUrl={groupAvatarUrl}
                      groupName={groupName}
                      onClick={onGroupHeaderItemClick}
                      width={28}
                      height={28}
                  />;
                  if (summary.profile && summary.profile.name) {
                      nameNode = <div onClick={onGroupHeaderItemClick}>
                          <span>{ summary.profile.name }</span>
                          <span className="mx_GroupView_header_groupid">
                              ({ this.props.groupId })
                          </span>
                      </div>;
                  } else {
                      nameNode = <span onClick={onGroupHeaderItemClick}>{ this.props.groupId }</span>;
                  }
                  if (summary.profile && summary.profile.short_description) {
                      shortDescNode = <span onClick={onGroupHeaderItemClick}>{ summary.profile.short_description }</span>;
                  }
              }
  
              if (this.state.editing) {
                  rightButtons.push(
                      <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
                          key="_saveButton"
                          onClick={this._onSaveClick}
                      >
                          { _t('Save') }
                      </AccessibleButton>,
                  );
                  rightButtons.push(
                      <AccessibleButton className="mx_RoomHeader_cancelButton"
                          key="_cancelButton"
                          onClick={this._onCancelClick}
                      >
                          <img
                              src={require("../../../res/img/cancel.svg")}
                              className="mx_filterFlipColor"
                              width="18"
                              height="18"
                              alt={_t("Cancel")} />
                      </AccessibleButton>,
                  );
              } else {
                  if (summary.user && summary.user.membership === 'join') {
                      rightButtons.push(
                          <AccessibleButton
                              className="mx_GroupHeader_button mx_GroupHeader_editButton"
                              key="_editButton"
                              onClick={this._onEditClick}
                              title={_t("Community Settings")}
                          />,
                      );
                  }
                  rightButtons.push(
                      <AccessibleButton
                          className="mx_GroupHeader_button mx_GroupHeader_shareButton"
                          key="_shareButton"
                          onClick={this._onShareClick}
                          title={_t('Share Community')}
                      />,
                  );
              }
  
              const rightPanel = this.state.showRightPanel ? <RightPanel groupId={this.props.groupId} /> : undefined;
  
              const headerClasses = {
                  "mx_GroupView_header": true,
                  "light-panel": true,
                  "mx_GroupView_header_view": !this.state.editing,
                  "mx_GroupView_header_isUserMember": this.state.isUserMember,
              };
  
              return (
                  <main className="mx_GroupView">
                      <div className={classnames(headerClasses)}>
                          <div className="mx_GroupView_header_leftCol">
                              <div className="mx_GroupView_header_avatar">
                                  { avatarNode }
                              </div>
                              <div className="mx_GroupView_header_info">
                                  <div className="mx_GroupView_header_name">
                                      { nameNode }
                                  </div>
                                  <div className="mx_GroupView_header_shortDesc">
                                      { shortDescNode }
                                  </div>
                              </div>
                          </div>
                          <div className="mx_GroupView_header_rightCol">
                              { rightButtons }
                          </div>
                          <GroupHeaderButtons />
                      </div>
                      <MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
                          <AutoHideScrollbar className="mx_GroupView_body">
                              { this._getMembershipSection() }
                              { this._getGroupSection() }
                          </AutoHideScrollbar>
                      </MainSplit>
                  </main>
              );
          } else if (this.state.error) {
              if (this.state.error.httpStatus === 404) {
                  return (
                      <div className="mx_GroupView_error">
                          { _t('Community %(groupId)s not found', { groupId: this.props.groupId }) }
                      </div>
                  );
              } else {
                  let extraText;
                  if (this.state.error.errcode === 'M_UNRECOGNIZED') {
                      extraText = <div>{ _t('This homeserver does not support communities') }</div>;
                  }
                  return (
                      <div className="mx_GroupView_error">
                          { _t('Failed to load %(groupId)s', { groupId: this.props.groupId }) }
                          { extraText }
                      </div>
                  );
              }
          } else {
              logger.error("Invalid state for GroupView");
              return <div />;
          }
      }
  }
  