diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index aab7701f26..3073397fba 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -390,7 +390,7 @@ export class ContextMenu extends React.PureComponent { } // Placement method for to position context menu to right of elementRect with chevronOffset -export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => { +export const toRightOf = (elementRect: Pick, chevronOffset = 12) => { const left = elementRect.right + window.pageXOffset + 3; let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset; top -= chevronOffset + 8; // where 8 is half the height of the chevron diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 98d69a63e7..0e16d17da9 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,7 +13,7 @@ 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 React, {ComponentProps} from 'react'; import Room from 'matrix-js-sdk/src/models/room'; import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo'; @@ -24,7 +24,7 @@ import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; import {ResizeMethod} from "../../../Avatar"; -interface IProps { +interface IProps extends Omit, "name" | "idName" | "url" | "onClick">{ // Room may be left unset here, but if it is, // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx new file mode 100644 index 0000000000..9178155d19 --- /dev/null +++ b/src/components/views/elements/RoomName.tsx @@ -0,0 +1,40 @@ +/* +Copyright 2021 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 {useEffect, useState} from "react"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import {useEventEmitter} from "../../../hooks/useEventEmitter"; + +interface IProps { + room: Room; + children?(name: string): JSX.Element; +} + +const RoomName = ({ room, children }: IProps): JSX.Element => { + const [name, setName] = useState(room?.name); + useEventEmitter(room, "Room.name", () => { + setName(room?.name); + }); + useEffect(() => { + setName(room?.name); + }, [room]); + + if (children) return children(name); + return name || ""; +}; + +export default RoomName; diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx new file mode 100644 index 0000000000..fe8aa5a83d --- /dev/null +++ b/src/components/views/elements/RoomTopic.tsx @@ -0,0 +1,45 @@ +/* +Copyright 2021 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, {useEffect, useState} from "react"; +import {EventType} from "matrix-js-sdk/src/@types/event"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {linkifyElement} from "../../../HtmlUtils"; + +interface IProps { + room?: Room; + children?(topic: string, ref: (element: HTMLElement) => void): JSX.Element; +} + +export const getTopic = room => room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; + +const RoomTopic = ({ room, children }: IProps): JSX.Element => { + const [topic, setTopic] = useState(getTopic(room)); + useEventEmitter(room.currentState, "RoomState.events", () => { + setTopic(getTopic(room)); + }); + useEffect(() => { + setTopic(getTopic(room)); + }, [room]); + + const ref = e => e && linkifyElement(e); + if (children) return children(topic, ref); + return { topic }; +}; + +export default RoomTopic; diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 9da6e22847..495a0f0d2c 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -450,17 +450,7 @@ export default class MemberList extends React.Component { let inviteButton; if (room && room.getMyMembership() === 'join') { - // assume we can invite until proven false - let canInvite = true; - - const plEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - const me = room.getMember(cli.getUserId()); - if (plEvent && me) { - const content = plEvent.getContent(); - if (content && content.invite > me.powerLevel) { - canInvite = false; - } - } + const canInvite = room.canInvite(cli.getUserId()); let inviteButtonText = _t("Invite to this room"); const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 9be3d6be18..ce426a64ed 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -100,15 +100,8 @@ const NewRoomIntro = () => { }); } - let canInvite = inRoom; - const powerLevels = room.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); - const me = room.getMember(cli.getUserId()); - if (powerLevels && me && powerLevels.invite > me.powerLevel) { - canInvite = false; - } - let buttons; - if (canInvite) { + if (room.canInvite(cli.getUserId())) { const onInviteClick = () => { dis.dispatch({ action: "view_invite", roomId }); }; diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 8eb8276630..93055c69f5 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -15,14 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import RateLimitedFunc from '../../../ratelimitedfunc'; -import { linkifyElement } from '../../../HtmlUtils'; import {CancelButton} from './SimpleRoomHeader'; import SettingsStore from "../../../settings/SettingsStore"; import RoomHeaderButtons from '../right_panel/RoomHeaderButtons'; @@ -30,6 +29,8 @@ import E2EIcon from './E2EIcon'; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import {DefaultTagID} from "../../../stores/room-list/models"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import RoomTopic from "../elements/RoomTopic"; +import RoomName from "../elements/RoomName"; export default class RoomHeader extends React.Component { static propTypes = { @@ -52,35 +53,13 @@ export default class RoomHeader extends React.Component { onCancelClick: null, }; - constructor(props) { - super(props); - - this._topic = createRef(); - } - componentDidMount() { const cli = MatrixClientPeg.get(); cli.on("RoomState.events", this._onRoomStateEvents); cli.on("Room.accountData", this._onRoomAccountData); - - // When a room name occurs, RoomState.events is fired *before* - // room.name is updated. So we have to listen to Room.name as well as - // RoomState.events. - if (this.props.room) { - this.props.room.on("Room.name", this._onRoomNameChange); - } - } - - componentDidUpdate() { - if (this._topic.current) { - linkifyElement(this._topic.current); - } } componentWillUnmount() { - if (this.props.room) { - this.props.room.removeListener("Room.name", this._onRoomNameChange); - } const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this._onRoomStateEvents); @@ -109,10 +88,6 @@ export default class RoomHeader extends React.Component { this.forceUpdate(); }, 500); - _onRoomNameChange = (room) => { - this.forceUpdate(); - }; - _hasUnreadPins() { const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", ''); if (!currentPinEvent) return false; @@ -170,29 +145,28 @@ export default class RoomHeader extends React.Component { } } - let roomName = _t("Join Room"); + let oobName = _t("Join Room"); if (this.props.oobData && this.props.oobData.name) { - roomName = this.props.oobData.name; - } else if (this.props.room) { - roomName = this.props.room.name; + oobName = this.props.oobData.name; } const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint }); const name =
-
{ roomName }
+ + {(name) => { + const roomName = name || oobName; + return
{ roomName }
; + }} +
{ searchStatus }
; - let topic; - if (this.props.room) { - const ev = this.props.room.currentState.getStateEvents('m.room.topic', ''); - if (ev) { - topic = ev.getContent().topic; - } - } - const topicElement = -
{ topic }
; + const topicElement = + {(topic, ref) =>
+ { topic } +
} +
; let roomAvatar; if (this.props.room) {