Merge branch 'develop' into bwindels/make-reply-not-overlay

This commit is contained in:
Bruno Windels 2020-08-18 13:39:06 +02:00
commit 5d98805d77
278 changed files with 6448 additions and 3045 deletions

View file

@ -242,7 +242,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
// so trigger a model update after the composition is done by calling the input handler.
// however, modifying the DOM (caused by the editor model update) from the compositionend handler seems
// to confuse the IME in Chrome, likely causing https://github.com/vector-im/riot-web/issues/10913 ,
// to confuse the IME in Chrome, likely causing https://github.com/vector-im/element-web/issues/10913 ,
// so we do it async
// however, doing this async seems to break things in Safari for some reason, so browser sniff.
@ -273,7 +273,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const {model} = this.props;
const range = getRangeForSelection(this.editorRef.current, model, selection);
const selectedParts = range.parts.map(p => p.serialize());
event.clipboardData.setData("application/x-riot-composer", JSON.stringify(selectedParts));
event.clipboardData.setData("application/x-element-composer", JSON.stringify(selectedParts));
event.clipboardData.setData("text/plain", text); // so plain copy/paste works
if (type === "cut") {
// Remove the text, updating the model as appropriate
@ -301,7 +301,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const {model} = this.props;
const {partCreator} = model;
const partsText = event.clipboardData.getData("application/x-riot-composer");
const partsText = event.clipboardData.getData("application/x-element-composer");
let parts;
if (partsText) {
const serializedTextParts = JSON.parse(partsText);

View file

@ -60,7 +60,7 @@ const stateEventTileTypes = {
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent',
'm.room.server_acl': 'messages.TextualEvent',
// TODO: Enable support for m.widget event type (https://github.com/vector-im/riot-web/issues/13111)
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
'im.vector.modular.widgets': 'messages.TextualEvent',
'm.room.tombstone': 'messages.TextualEvent',
'm.room.join_rules': 'messages.TextualEvent',
@ -519,7 +519,7 @@ export default createReactClass({
onPermalinkClicked: function(e) {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked.
// matrix.to, but also for it to enable routing within Element when clicked.
e.preventDefault();
dis.dispatch({
action: 'view_room',
@ -595,11 +595,11 @@ export default createReactClass({
}
const eventId = this.props.mxEvent.getId();
if (!eventId) {
// XXX: Temporary diagnostic logging for https://github.com/vector-im/riot-web/issues/11120
// XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120
console.error("EventTile attempted to get relations for an event without an ID");
// Use event's special `toJSON` method to log key data.
console.log(JSON.stringify(this.props.mxEvent, null, 4));
console.trace("Stacktrace for https://github.com/vector-im/riot-web/issues/11120");
console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120");
}
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
},

View file

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
@ -118,9 +119,19 @@ const EmojiButton = ({addEmoji}) => {
</ContextMenu>;
}
const className = classNames(
"mx_MessageComposer_button",
"mx_MessageComposer_emoji",
{
"mx_MessageComposer_button_highlight": menuDisplayed,
},
);
// TODO: replace ContextMenuTooltipButton with a unified representation of
// the header buttons and the right panel buttons
return <React.Fragment>
<ContextMenuTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_emoji"
className={className}
onClick={openMenu}
isExpanded={menuDisplayed}
title={_t('Emoji picker')}

View file

@ -97,7 +97,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
// Don't show a badge if we don't need to
if (notification.isIdle) return null;
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/element-web/issues/14261
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
// See git diff for what that boolean state looks like.
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).

View file

@ -73,7 +73,7 @@ export default class ReplyPreview extends React.Component {
return <div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
{ '💬 ' + _t('Replying') }
{ _t('Replying') }
</div>
<div className="mx_ReplyPreview_header mx_ReplyPreview_cancel">
<img className="mx_filterFlipColor" src={require("../../../../res/img/cancel.svg")} width="18" height="18"

View file

@ -43,6 +43,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
@ -81,6 +82,7 @@ interface ITagAesthetics {
sectionLabelRaw?: string;
addRoomLabel?: string;
onAddRoom?: (dispatcher?: Dispatcher<ActionPayload>) => void;
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
isInvite: boolean;
defaultHidden: boolean;
}
@ -112,9 +114,30 @@ const TAG_AESTHETICS: {
sectionLabel: _td("Rooms"),
isInvite: false,
defaultHidden: false,
addRoomLabel: _td("Create room"),
onAddRoom: (dispatcher?: Dispatcher<ActionPayload>) => {
(dispatcher || defaultDispatcher).dispatch({action: 'view_create_room'})
addRoomLabel: _td("Add room"),
addRoomContextMenu: (onFinished: () => void) => {
return <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.dispatch({action: "view_create_room"});
}}
/>
<IconizedContextMenuOption
label={_t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
defaultDispatcher.fire(Action.ViewRoomDirectory);
}}
/>
</IconizedContextMenuOptionList>;
},
},
[DefaultTagID.LowPriority]: {
@ -128,7 +151,7 @@ const TAG_AESTHETICS: {
defaultHidden: false,
},
// TODO: Replace with archived view: https://github.com/vector-im/riot-web/issues/14038
// TODO: Replace with archived view: https://github.com/vector-im/element-web/issues/14038
[DefaultTagID.Archived]: {
sectionLabel: _td("Historical"),
isInvite: false,
@ -215,7 +238,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private updateLists = () => {
const newLists = RoomListStore.instance.orderedLists;
if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
console.log("new lists", newLists);
}
@ -245,6 +268,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
if (doUpdate) {
// We have to break our reference to the room list store if we want to be able to
// diff the object for changes, so do that.
// @ts-ignore - ITagMap is ts-ignored so this will have to be too
const newSublists = objectWithOnly(newLists, newListIds);
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
@ -256,7 +280,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private renderCommunityInvites(): TemporaryTile[] {
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/riot-web/issues/14456
// See https://github.com/vector-im/element-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {
return g.myMembership === 'invite';
}).map(g => {
@ -323,6 +347,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={aesthetics.onAddRoom}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
addRoomContextMenu={aesthetics.addRoomContextMenu}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles}

View file

@ -511,7 +511,7 @@ export default createReactClass({
"If you think you're seeing this message in error, please " +
"<issueLink>submit a bug report</issueLink>.",
{ errcode: this.props.error.errcode },
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
{ issueLink: label => <a href="https://github.com/vector-im/element-web/issues/new/choose"
target="_blank" rel="noreferrer noopener">{ label }</a> },
),
];

View file

@ -21,7 +21,8 @@ import * as sdk from "../../../index";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
export default class RoomRecoveryReminder extends React.PureComponent {
static propTypes = {

View file

@ -50,6 +50,7 @@ import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
import TemporaryTile from "./TemporaryTile";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
@ -65,6 +66,7 @@ interface IProps {
startAsHidden: boolean;
label: string;
onAddRoom?: () => void;
addRoomContextMenu?: (onFinished: () => void) => React.ReactNode;
addRoomLabel: string;
isMinimized: boolean;
tagId: TagID;
@ -74,7 +76,7 @@ interface IProps {
// You should feel bad if you use this.
extraBadTilesThatShouldntExist?: TemporaryTile[];
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
}
// TODO: Use re-resizer's NumberSize when it is exposed as the type
@ -87,6 +89,7 @@ type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
interface IState {
contextMenuPosition: PartialDOMRect;
addRoomContextMenuPosition: PartialDOMRect;
isResizing: boolean;
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
height: number;
@ -112,6 +115,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
this.state = {
contextMenuPosition: null,
addRoomContextMenuPosition: null,
isResizing: false,
isExpanded: this.isBeingFiltered ? this.isBeingFiltered : !this.layout.isCollapsed,
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
@ -376,10 +380,21 @@ export default class RoomSublist extends React.Component<IProps, IState> {
});
};
private onAddRoomContextMenu = (ev: React.MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
this.setState({addRoomContextMenuPosition: target.getBoundingClientRect()});
};
private onCloseMenu = () => {
this.setState({contextMenuPosition: null});
};
private onCloseAddRoomMenu = () => {
this.setState({addRoomContextMenuPosition: null});
};
private onUnreadFirstChanged = async () => {
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
@ -594,6 +609,18 @@ export default class RoomSublist extends React.Component<IProps, IState> {
</div>
</ContextMenu>
);
} else if (this.state.addRoomContextMenuPosition) {
contextMenu = (
<IconizedContextMenu
chevronFace={ChevronFace.None}
left={this.state.addRoomContextMenuPosition.left - 7} // center align with the handle
top={this.state.addRoomContextMenuPosition.top + this.state.addRoomContextMenuPosition.height}
onFinished={this.onCloseAddRoomMenu}
compact
>
{this.props.addRoomContextMenu(this.onCloseAddRoomMenu)}
</IconizedContextMenu>
);
}
return (
@ -637,9 +664,21 @@ export default class RoomSublist extends React.Component<IProps, IState> {
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
tooltipClassName={"mx_RoomSublist_addRoomTooltip"}
/>
);
} else if (this.props.addRoomContextMenu) {
addRoomButton = (
<ContextMenuTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoomContextMenu}
className="mx_RoomSublist_auxButton"
tooltipClassName="mx_RoomSublist_addRoomTooltip"
aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel}
isExpanded={!!this.state.addRoomContextMenuPosition}
/>
);
}
@ -703,7 +742,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
private onScrollPrevent(e: React.UIEvent<HTMLDivElement>) {
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
// this fixes https://github.com/vector-im/riot-web/issues/14413
// this fixes https://github.com/vector-im/element-web/issues/14413
(e.target as HTMLDivElement).scrollTop = 0;
}
@ -738,14 +777,20 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
const numMissing = this.numTiles - amountFullyShown;
const label = _t("Show %(count)s more", {count: numMissing});
let showMoreText = (
<span className='mx_RoomSublist_showNButtonText'>
{_t("Show %(count)s more", {count: numMissing})}
{label}
</span>
);
if (this.props.isMinimized) showMoreText = null;
showNButton = (
<RovingAccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses}>
<RovingAccessibleButton
role="treeitem"
onClick={this.onShowAllClick}
className={showMoreBtnClasses}
aria-label={label}
>
<span className='mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron'>
{/* set by CSS masking */}
</span>
@ -754,14 +799,20 @@ export default class RoomSublist extends React.Component<IProps, IState> {
);
} else if (this.numTiles > this.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less
const label = _t("Show less");
let showLessText = (
<span className='mx_RoomSublist_showNButtonText'>
{_t("Show less")}
{label}
</span>
);
if (this.props.isMinimized) showLessText = null;
showNButton = (
<RovingAccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses}>
<RovingAccessibleButton
role="treeitem"
onClick={this.onShowLessClick}
className={showMoreBtnClasses}
aria-label={label}
>
<span className='mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron'>
{/* set by CSS masking */}
</span>

View file

@ -17,44 +17,38 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from "react";
import React, { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from "classnames";
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
import dis from '../../../dispatcher/dispatcher';
import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
import {
ChevronFace,
ContextMenu,
ContextMenuTooltipButton,
MenuItemRadio,
MenuItemCheckbox,
MenuItem,
} from "../../structures/ContextMenu";
import { ChevronFace, ContextMenuTooltipButton, MenuItemRadio } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {
getRoomNotifsState,
setRoomNotifsState,
ALL_MESSAGES,
ALL_MESSAGES_LOUD,
MENTIONS_ONLY,
MUTE,
} from "../../../RoomNotifs";
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE, } from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import NotificationBadge from "./NotificationBadge";
import { Volume } from "../../../RoomNotifsTypes";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../actions/RoomListActions";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ActionPayload} from "../../../dispatcher/payloads";
import { ActionPayload } from "../../../dispatcher/payloads";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { CachedRoomKey, RoomEchoChamber } from "../../../stores/local-echo/RoomEchoChamber";
import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber";
import IconizedContextMenu, {
IconizedContextMenuCheckbox,
IconizedContextMenuOption,
IconizedContextMenuOptionList, IconizedContextMenuRadio
} from "../context_menus/IconizedContextMenu";
interface IProps {
room: Room;
@ -82,36 +76,11 @@ const contextMenuBelow = (elementRect: PartialDOMRect) => {
return {left, top, chevronFace};
};
interface INotifOptionProps {
active: boolean;
iconClassName: string;
label: string;
onClick(ev: ButtonEvent);
}
const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassName, label}) => {
const classes = classNames({
mx_RoomTile_contextMenu_activeRow: active,
});
let activeIcon;
if (active) {
activeIcon = <span className="mx_IconizedContextMenu_icon mx_RoomTile_iconCheck" />;
}
return (
<MenuItemRadio className={classes} onClick={onClick} active={active} label={label}>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
<span className="mx_IconizedContextMenu_label">{ label }</span>
{ activeIcon }
</MenuItemRadio>
);
};
export default class RoomTile extends React.PureComponent<IProps, IState> {
private dispatcherRef: string;
private roomTileRef = createRef<HTMLDivElement>();
private notificationState: NotificationState;
private roomProps: RoomEchoChamber;
constructor(props: IProps) {
super(props);
@ -130,12 +99,19 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
MessagePreviewStore.instance.on(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.roomProps = EchoChamber.forRoom(this.props.room);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
}
private onNotificationUpdate = () => {
this.forceUpdate(); // notification state changed - update
};
private onRoomPropertyUpdate = (property: CachedRoomKey) => {
if (property === CachedRoomKey.NotificationVolume) this.onNotificationUpdate();
// else ignore - not important for this tile
};
private get showContextMenu(): boolean {
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
}
@ -144,6 +120,12 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
return !this.props.isMinimized && this.props.showMessagePreview;
}
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>) {
if (prevProps.showMessagePreview !== this.props.showMessagePreview && this.showMessagePreview) {
this.setState({messagePreview: this.generatePreview()});
}
}
public componentDidMount() {
// when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
if (this.state.selected) {
@ -307,17 +289,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
ev.stopPropagation();
if (MatrixClientPeg.get().isGuest()) return;
// get key before we go async and React discards the nativeEvent
const key = (ev as React.KeyboardEvent).key;
try {
// TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
await setRoomNotifsState(this.props.room.roomId, newState);
} catch (error) {
// TODO: some form of error notification to the user to inform them that their state change failed.
// See https://github.com/vector-im/riot-web/issues/14281
console.error(error);
}
this.roomProps.notificationVolume = newState;
const key = (ev as React.KeyboardEvent).key;
if (key === Key.ENTER) {
// Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
this.setState({notificationsMenuPosition: null}); // hide the menu
@ -335,42 +309,43 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
return null;
}
const state = getRoomNotifsState(this.props.room.roomId);
const state = this.roomProps.notificationVolume;
let contextMenu = null;
if (this.state.notificationsMenuPosition) {
contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.notificationsMenuPosition)} onFinished={this.onCloseNotificationsMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<NotifOption
label={_t("Use default")}
active={state === ALL_MESSAGES}
iconClassName="mx_RoomTile_iconBell"
onClick={this.onClickAllNotifs}
/>
<NotifOption
label={_t("All messages")}
active={state === ALL_MESSAGES_LOUD}
iconClassName="mx_RoomTile_iconBellDot"
onClick={this.onClickAlertMe}
/>
<NotifOption
label={_t("Mentions & Keywords")}
active={state === MENTIONS_ONLY}
iconClassName="mx_RoomTile_iconBellMentions"
onClick={this.onClickMentions}
/>
<NotifOption
label={_t("None")}
active={state === MUTE}
iconClassName="mx_RoomTile_iconBellCrossed"
onClick={this.onClickMute}
/>
</div>
</div>
</ContextMenu>
);
contextMenu = <IconizedContextMenu
{...contextMenuBelow(this.state.notificationsMenuPosition)}
onFinished={this.onCloseNotificationsMenu}
className="mx_RoomTile_contextMenu"
compact
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuRadio
label={_t("Use default")}
active={state === ALL_MESSAGES}
iconClassName="mx_RoomTile_iconBell"
onClick={this.onClickAllNotifs}
/>
<IconizedContextMenuRadio
label={_t("All messages")}
active={state === ALL_MESSAGES_LOUD}
iconClassName="mx_RoomTile_iconBellDot"
onClick={this.onClickAlertMe}
/>
<IconizedContextMenuRadio
label={_t("Mentions & Keywords")}
active={state === MENTIONS_ONLY}
iconClassName="mx_RoomTile_iconBellMentions"
onClick={this.onClickMentions}
/>
<IconizedContextMenuRadio
label={_t("None")}
active={state === MUTE}
iconClassName="mx_RoomTile_iconBellCrossed"
onClick={this.onClickMute}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
const classes = classNames("mx_RoomTile_notificationsButton", {
@ -404,18 +379,20 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
let contextMenu = null;
if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList mx_RoomTile_contextMenu_redRow">
<MenuItem onClick={this.onForgetRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Forget Room")}</span>
</MenuItem>
</div>
</div>
</ContextMenu>
);
contextMenu = <IconizedContextMenu
{...contextMenuBelow(this.state.generalMenuPosition)}
onFinished={this.onCloseGeneralMenu}
className="mx_RoomTile_contextMenu"
compact
>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
iconClassName="mx_RoomTile_iconSignOut"
label={_t("Forget Room")}
onClick={this.onForgetRoomClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
} else if (this.state.generalMenuPosition) {
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
@ -425,42 +402,40 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
const lowPriorityLabel = _t("Low Priority");
contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<MenuItemCheckbox
className={isFavorite ? "mx_RoomTile_contextMenu_activeRow" : ""}
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
active={isFavorite}
label={favouriteLabel}
>
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconStar" />
<span className="mx_IconizedContextMenu_label">{favouriteLabel}</span>
</MenuItemCheckbox>
<MenuItemCheckbox
className={isLowPriority ? "mx_RoomTile_contextMenu_activeRow" : ""}
onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}
active={isLowPriority}
label={lowPriorityLabel}
>
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconArrowDown" />
<span className="mx_IconizedContextMenu_label">{lowPriorityLabel}</span>
</MenuItemCheckbox>
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
</MenuItem>
</div>
<div className="mx_IconizedContextMenu_optionList mx_RoomTile_contextMenu_redRow">
<MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
</MenuItem>
</div>
</div>
</ContextMenu>
);
contextMenu = <IconizedContextMenu
{...contextMenuBelow(this.state.generalMenuPosition)}
onFinished={this.onCloseGeneralMenu}
className="mx_RoomTile_contextMenu"
compact
>
<IconizedContextMenuOptionList>
<IconizedContextMenuCheckbox
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
active={isFavorite}
label={favouriteLabel}
iconClassName="mx_RoomTile_iconStar"
/>
<IconizedContextMenuCheckbox
onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}
active={isLowPriority}
label={lowPriorityLabel}
iconClassName="mx_RoomTile_iconArrowDown"
/>
<IconizedContextMenuOption
onClick={this.onOpenRoomSettings}
label={_t("Settings")}
iconClassName="mx_RoomTile_iconSettings"
/>
</IconizedContextMenuOptionList>
<IconizedContextMenuOptionList red>
<IconizedContextMenuOption
onClick={this.onLeaveRoomClick}
label={_t("Leave Room")}
iconClassName="mx_RoomTile_iconSignOut"
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>;
}
return (

View file

@ -1,168 +0,0 @@
/*
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 from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { User } from "matrix-js-sdk/src/models/user";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import DMRoomMap from "../../../utils/DMRoomMap";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { isPresenceEnabled } from "../../../utils/presence";
import { _t } from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip";
enum Icon {
// Note: the names here are used in CSS class names
None = "NONE", // ... except this one
Globe = "GLOBE",
PresenceOnline = "ONLINE",
PresenceAway = "AWAY",
PresenceOffline = "OFFLINE",
}
function tooltipText(variant: Icon) {
switch (variant) {
case Icon.Globe:
return _t("This room is public");
case Icon.PresenceOnline:
return _t("Online");
case Icon.PresenceAway:
return _t("Away");
case Icon.PresenceOffline:
return _t("Offline");
}
}
interface IProps {
room: Room;
}
interface IState {
icon: Icon;
}
export default class RoomTileIcon extends React.Component<IProps, IState> {
private _dmUser: User;
private isUnmounted = false;
private isWatchingTimeline = false;
constructor(props: IProps) {
super(props);
this.state = {
icon: this.calculateIcon(),
};
}
private get isPublicRoom(): boolean {
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule;
return joinRule === 'public';
}
private get dmUser(): User {
return this._dmUser;
}
private set dmUser(val: User) {
const oldUser = this._dmUser;
this._dmUser = val;
if (oldUser && oldUser !== this._dmUser) {
oldUser.off('User.currentlyActive', this.onPresenceUpdate);
oldUser.off('User.presence', this.onPresenceUpdate);
}
if (this._dmUser && oldUser !== this._dmUser) {
this._dmUser.on('User.currentlyActive', this.onPresenceUpdate);
this._dmUser.on('User.presence', this.onPresenceUpdate);
}
}
public componentWillUnmount() {
this.isUnmounted = true;
if (this.isWatchingTimeline) this.props.room.off('Room.timeline', this.onRoomTimeline);
this.dmUser = null; // clear listeners, if any
}
private onRoomTimeline = (ev: MatrixEvent, room: Room) => {
if (this.isUnmounted) return;
// apparently these can happen?
if (!room) return;
if (this.props.room.roomId !== room.roomId) return;
if (ev.getType() === 'm.room.join_rules' || ev.getType() === 'm.room.member') {
this.setState({icon: this.calculateIcon()});
}
};
private onPresenceUpdate = () => {
if (this.isUnmounted) return;
let newIcon = this.getPresenceIcon();
if (newIcon !== this.state.icon) this.setState({icon: newIcon});
};
private getPresenceIcon(): Icon {
if (!this.dmUser) return Icon.None;
let icon = Icon.None;
const isOnline = this.dmUser.currentlyActive || this.dmUser.presence === 'online';
if (isOnline) {
icon = Icon.PresenceOnline;
} else if (this.dmUser.presence === 'offline') {
icon = Icon.PresenceOffline;
} else if (this.dmUser.presence === 'unavailable') {
icon = Icon.PresenceAway;
}
return icon;
}
private calculateIcon(): Icon {
let icon = Icon.None;
// We look at the DMRoomMap and not the tag here so that we don't exclude DMs in Favourites
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
if (otherUserId && this.props.room.getJoinedMemberCount() === 2) {
// Track presence, if available
if (isPresenceEnabled()) {
if (otherUserId) {
this.dmUser = MatrixClientPeg.get().getUser(otherUserId);
icon = this.getPresenceIcon();
}
}
} else {
// Track publicity
icon = this.isPublicRoom ? Icon.Globe : Icon.None;
if (!this.isWatchingTimeline) {
this.props.room.on('Room.timeline', this.onRoomTimeline);
this.isWatchingTimeline = true;
}
}
return icon;
}
public render(): React.ReactElement {
if (this.state.icon === Icon.None) return null;
return <TextWithTooltip
tooltip={tooltipText(this.state.icon)}
class={`mx_RoomTileIcon mx_RoomTileIcon_${this.state.icon.toLowerCase()}`}
/>;
}
}

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import {_t, _td} from '../../../languageHandler';
import AppTile from '../elements/AppTile';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
@ -380,14 +381,21 @@ export default class Stickerpicker extends React.Component {
render() {
let stickerPicker;
let stickersButton;
const className = classNames(
"mx_MessageComposer_button",
"mx_MessageComposer_stickers",
"mx_Stickers_hideStickers",
"mx_MessageComposer_button_highlight",
);
if (this.state.showStickers) {
// Show hide-stickers button
stickersButton =
<AccessibleButton
id='stickersButton'
key="controls_hide_stickers"
className="mx_MessageComposer_button mx_MessageComposer_stickers mx_Stickers_hideStickers"
className={className}
onClick={this._onHideStickersClick}
active={this.state.showStickers}
title={_t("Hide Stickers")}
>
</AccessibleButton>;

View file

@ -38,7 +38,7 @@ interface IState {
hover: boolean;
}
// TODO: Remove with community invites in the room list: https://github.com/vector-im/riot-web/issues/14456
// TODO: Remove with community invites in the room list: https://github.com/vector-im/element-web/issues/14456
export default class TemporaryTile extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);