Merge branches 'develop' and 't3chguy/toasts3' of github.com:matrix-org/matrix-react-sdk into t3chguy/toasts3
Conflicts: src/@types/global.d.ts src/components/structures/ToastContainer.tsx
This commit is contained in:
commit
c74d890774
56 changed files with 2052 additions and 733 deletions
|
@ -92,7 +92,7 @@ const CategoryRoomList = createReactClass({
|
|||
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 alias"),
|
||||
placeholder: _t("Room name or address"),
|
||||
button: _t("Add to summary"),
|
||||
pickerType: 'room',
|
||||
validAddressTypes: ['mx-room-id'],
|
||||
|
|
|
@ -29,7 +29,7 @@ import { fixupColorFonts } from '../../utils/FontManager';
|
|||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import sessionStore from '../../stores/SessionStore';
|
||||
import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import {MatrixClientPeg, IMatrixClientCreds} from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
|
@ -57,7 +57,7 @@ function canElementReceiveInput(el) {
|
|||
|
||||
interface IProps {
|
||||
matrixClient: MatrixClient;
|
||||
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
|
||||
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
|
||||
viaServers?: string[];
|
||||
hideToSRUsers: boolean;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
|
|
|
@ -932,9 +932,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private viewGroup(payload) {
|
||||
private async viewGroup(payload) {
|
||||
const groupId = payload.group_id;
|
||||
|
||||
// Wait for the first sync to complete
|
||||
if (!this.firstSyncComplete) {
|
||||
if (!this.firstSyncPromise) {
|
||||
console.warn('Cannot view a group before first sync. group_id:', groupId);
|
||||
return;
|
||||
}
|
||||
await this.firstSyncPromise.promise;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
view: Views.LOGGED_IN,
|
||||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
|
|
|
@ -199,7 +199,7 @@ export default createReactClass({
|
|||
|
||||
let desc;
|
||||
if (alias) {
|
||||
desc = _t('Delete the room alias %(alias)s and remove %(name)s from the directory?', {alias: alias, name: name});
|
||||
desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
|
||||
} else {
|
||||
desc = _t('Remove %(name)s from the directory?', {name: name});
|
||||
}
|
||||
|
@ -216,7 +216,7 @@ export default createReactClass({
|
|||
|
||||
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
|
||||
if (!alias) return;
|
||||
step = _t('delete the alias.');
|
||||
step = _t('delete the address.');
|
||||
return MatrixClientPeg.get().deleteAlias(alias);
|
||||
}).then(() => {
|
||||
modal.close();
|
||||
|
|
|
@ -71,7 +71,10 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
|||
toastKey: key,
|
||||
});
|
||||
toast = (<div className={toastClasses}>
|
||||
<h2>{title}{countIndicator}</h2>
|
||||
<div className="mx_Toast_title">
|
||||
<h2>{title}</h2>
|
||||
<span>{countIndicator}</span>
|
||||
</div>
|
||||
<div className="mx_Toast_body">{React.createElement(component, toastProps)}</div>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 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.
|
||||
|
@ -17,220 +17,182 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import createReactClass from 'create-react-class';
|
||||
import * as AvatarLogic from '../../../Avatar';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
|
||||
export default createReactClass({
|
||||
displayName: 'BaseAvatar',
|
||||
const useImageUrl = ({url, urls}) => {
|
||||
const [imageUrls, setUrls] = useState([]);
|
||||
const [urlsIndex, setIndex] = useState();
|
||||
|
||||
propTypes: {
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
},
|
||||
const onError = useCallback(() => {
|
||||
setIndex(i => i + 1); // try the next one
|
||||
}, []);
|
||||
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
statics: {
|
||||
contextType: MatrixClientContext,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
width: 40,
|
||||
height: 40,
|
||||
resizeMethod: 'crop',
|
||||
defaultToInitialLetter: true,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.unmounted = false;
|
||||
this.context.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
UNSAFE_componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
const newState = this._getState(nextProps);
|
||||
const newImageUrls = newState.imageUrls;
|
||||
const oldImageUrls = this.state.imageUrls;
|
||||
if (newImageUrls.length !== oldImageUrls.length) {
|
||||
this.setState(newState); // detected a new entry
|
||||
} else {
|
||||
// check each one to see if they are the same
|
||||
for (let i = 0; i < newImageUrls.length; i++) {
|
||||
if (oldImageUrls[i] !== newImageUrls[i]) {
|
||||
this.setState(newState); // detected a diff
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onClientSync: function(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
this.state.urlsIndex > 0
|
||||
) {
|
||||
// Start from the highest priority URL again
|
||||
this.setState({
|
||||
urlsIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getState: function(props) {
|
||||
useEffect(() => {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
// imageUrls: [ props.url, ...props.urls ]
|
||||
|
||||
let urls = [];
|
||||
let _urls = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
urls = props.urls || [];
|
||||
_urls = memoizedUrls || [];
|
||||
|
||||
if (props.url) {
|
||||
urls.unshift(props.url); // put in urls[0]
|
||||
if (url) {
|
||||
_urls.unshift(url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
let defaultImageUrl = null;
|
||||
if (props.defaultToInitialLetter) {
|
||||
defaultImageUrl = AvatarLogic.defaultAvatarUrlForString(
|
||||
props.idName || props.name,
|
||||
);
|
||||
urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
|
||||
// deduplicate URLs
|
||||
urls = Array.from(new Set(urls));
|
||||
_urls = Array.from(new Set(_urls));
|
||||
|
||||
return {
|
||||
imageUrls: urls,
|
||||
defaultImageUrl: defaultImageUrl,
|
||||
urlsIndex: 0,
|
||||
};
|
||||
},
|
||||
setIndex(0);
|
||||
setUrls(_urls);
|
||||
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
onError: function(ev) {
|
||||
const nextIndex = this.state.urlsIndex + 1;
|
||||
if (nextIndex < this.state.imageUrls.length) {
|
||||
// try the next one
|
||||
this.setState({
|
||||
urlsIndex: nextIndex,
|
||||
});
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const onClientSync = useCallback((syncState, prevState) => {
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected) {
|
||||
setIndex(0);
|
||||
}
|
||||
},
|
||||
}, []);
|
||||
useEventEmitter(cli, "sync", onClientSync);
|
||||
|
||||
render: function() {
|
||||
const imageUrl = this.state.imageUrls[this.state.urlsIndex];
|
||||
const imageUrl = imageUrls[urlsIndex];
|
||||
return [imageUrl, onError];
|
||||
};
|
||||
|
||||
const {
|
||||
name, idName, title, url, urls, width, height, resizeMethod,
|
||||
defaultToInitialLetter, onClick, inputRef,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
const BaseAvatar = (props) => {
|
||||
const {
|
||||
name,
|
||||
idName,
|
||||
title,
|
||||
url,
|
||||
urls,
|
||||
width=40,
|
||||
height=40,
|
||||
resizeMethod="crop", // eslint-disable-line no-unused-vars
|
||||
defaultToInitialLetter=true,
|
||||
onClick,
|
||||
inputRef,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [imageUrl, onError] = useImageUrl({url, urls});
|
||||
|
||||
if (!imageUrl && defaultToInitialLetter) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span
|
||||
className="mx_BaseAvatar_initial"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toPx(width * 0.65),
|
||||
width: toPx(width),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
>
|
||||
{ initialLetter }
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img
|
||||
className="mx_BaseAvatar_image"
|
||||
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
|
||||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
aria-hidden="true" />
|
||||
);
|
||||
|
||||
if (imageUrl === this.state.defaultImageUrl) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span className="mx_BaseAvatar_initial" aria-hidden="true"
|
||||
style={{
|
||||
fontSize: toPx(width * 0.65),
|
||||
width: toPx(width),
|
||||
lineHeight: toPx(height),
|
||||
}}
|
||||
>
|
||||
{ initialLetter }
|
||||
</span>
|
||||
);
|
||||
const imgNode = (
|
||||
<img className="mx_BaseAvatar_image" src={imageUrl}
|
||||
alt="" title={title} onError={this.onError}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height)
|
||||
}} />
|
||||
);
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton element='span' className="mx_BaseAvatar"
|
||||
onClick={onClick} inputRef={inputRef} {...otherProps}
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
{...otherProps}
|
||||
element="span"
|
||||
className="mx_BaseAvatar"
|
||||
onClick={onClick}
|
||||
onError={this.onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={this.onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
<span className="mx_BaseAvatar" ref={inputRef} {...otherProps}>
|
||||
{ textNode }
|
||||
{ imgNode }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (onClick != null) {
|
||||
return (
|
||||
<AccessibleButton
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
element='img'
|
||||
src={imageUrl}
|
||||
onClick={onClick}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
inputRef={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<img
|
||||
className="mx_BaseAvatar mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
onError={onError}
|
||||
style={{
|
||||
width: toPx(width),
|
||||
height: toPx(height),
|
||||
}}
|
||||
title={title} alt=""
|
||||
ref={inputRef}
|
||||
{...otherProps} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BaseAvatar.displayName = "BaseAvatar";
|
||||
|
||||
BaseAvatar.propTypes = {
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
onClick: PropTypes.func,
|
||||
inputRef: PropTypes.oneOfType([
|
||||
// Either a function
|
||||
PropTypes.func,
|
||||
// Or the instance of a DOM native element
|
||||
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
|
||||
]),
|
||||
};
|
||||
|
||||
export default BaseAvatar;
|
||||
|
|
|
@ -98,7 +98,7 @@ export default createReactClass({
|
|||
|
||||
render: function() {
|
||||
return (
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Alias (optional)")}
|
||||
<input type="text" className="mx_RoomAlias" placeholder={_t("Address (optional)")}
|
||||
onChange={this.onValueChanged} onFocus={this.onFocus} onBlur={this.onBlur}
|
||||
value={this.props.alias} />
|
||||
);
|
||||
|
|
|
@ -181,7 +181,7 @@ export default createReactClass({
|
|||
let publicPrivateLabel;
|
||||
let aliasField;
|
||||
if (this.state.isPublic) {
|
||||
publicPrivateLabel = (<p>{_t("Set a room alias to easily share your room with other people.")}</p>);
|
||||
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
aliasField = (
|
||||
<div className="mx_CreateRoomDialog_aliasContainer">
|
||||
|
|
|
@ -45,7 +45,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
|
||||
return (
|
||||
<Field
|
||||
label={_t("Room alias")}
|
||||
label={_t("Room address")}
|
||||
className="mx_RoomAliasField"
|
||||
prefix={poundSign}
|
||||
postfix={domain}
|
||||
|
@ -87,7 +87,7 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
}, {
|
||||
key: "required",
|
||||
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Please provide a room alias"),
|
||||
invalid: () => _t("Please provide a room address"),
|
||||
}, {
|
||||
key: "taken",
|
||||
final: true,
|
||||
|
@ -107,8 +107,8 @@ export default class RoomAliasField extends React.PureComponent {
|
|||
return !!err.errcode;
|
||||
}
|
||||
},
|
||||
valid: () => _t("This alias is available to use"),
|
||||
invalid: () => _t("This alias is already in use"),
|
||||
valid: () => _t("This address is available to use"),
|
||||
invalid: () => _t("This address is already in use"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -67,7 +67,13 @@ class Category extends React.PureComponent {
|
|||
const localScrollTop = Math.max(0, scrollTop - listTop);
|
||||
|
||||
return (
|
||||
<section className="mx_EmojiPicker_category" data-category-id={this.props.id}>
|
||||
<section
|
||||
id={`mx_EmojiPicker_category_${this.props.id}`}
|
||||
className="mx_EmojiPicker_category"
|
||||
data-category-id={this.props.id}
|
||||
role="tabpanel"
|
||||
aria-label={name}
|
||||
>
|
||||
<h2 className="mx_EmojiPicker_category_label">
|
||||
{name}
|
||||
</h2>
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MenuItem} from "../../structures/ContextMenu";
|
||||
|
||||
class Emoji extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -30,14 +31,18 @@ class Emoji extends React.PureComponent {
|
|||
const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props;
|
||||
const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode);
|
||||
return (
|
||||
<li onClick={() => onClick(emoji)}
|
||||
<MenuItem
|
||||
element="li"
|
||||
onClick={() => onClick(emoji)}
|
||||
onMouseEnter={() => onMouseEnter(emoji)}
|
||||
onMouseLeave={() => onMouseLeave(emoji)}
|
||||
className="mx_EmojiPicker_item_wrapper">
|
||||
className="mx_EmojiPicker_item_wrapper"
|
||||
label={emoji.unicode}
|
||||
>
|
||||
<div className={`mx_EmojiPicker_item ${isSelected ? 'mx_EmojiPicker_item_selected' : ''}`}>
|
||||
{emoji.unicode}
|
||||
</div>
|
||||
</li>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,8 +147,12 @@ class EmojiPicker extends React.Component {
|
|||
// We update this here instead of through React to avoid re-render on scroll.
|
||||
if (cat.visible) {
|
||||
cat.ref.current.classList.add("mx_EmojiPicker_anchor_visible");
|
||||
cat.ref.current.setAttribute("aria-selected", true);
|
||||
cat.ref.current.setAttribute("tabindex", 0);
|
||||
} else {
|
||||
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
|
||||
cat.ref.current.setAttribute("aria-selected", false);
|
||||
cat.ref.current.setAttribute("tabindex", -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,23 +16,89 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from "classnames";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
class Header extends React.PureComponent {
|
||||
static propTypes = {
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onAnchorClick: PropTypes.func.isRequired,
|
||||
refs: PropTypes.object,
|
||||
};
|
||||
|
||||
findNearestEnabled(index, delta) {
|
||||
index += this.props.categories.length;
|
||||
const cats = [...this.props.categories, ...this.props.categories, ...this.props.categories];
|
||||
|
||||
while (index < cats.length && index >= 0) {
|
||||
if (cats[index].enabled) return index % this.props.categories.length;
|
||||
index += delta > 0 ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
changeCategoryRelative(delta) {
|
||||
const current = this.props.categories.findIndex(c => c.visible);
|
||||
this.changeCategoryAbsolute(current + delta, delta);
|
||||
}
|
||||
|
||||
changeCategoryAbsolute(index, delta=1) {
|
||||
const category = this.props.categories[this.findNearestEnabled(index, delta)];
|
||||
if (category) {
|
||||
this.props.onAnchorClick(category.id);
|
||||
category.ref.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Implements ARIA Tabs with Automatic Activation pattern
|
||||
// https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-1/tabs.html
|
||||
onKeyDown = (ev) => {
|
||||
let handled = true;
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
this.changeCategoryRelative(-1);
|
||||
break;
|
||||
case Key.ARROW_RIGHT:
|
||||
this.changeCategoryRelative(1);
|
||||
break;
|
||||
|
||||
case Key.HOME:
|
||||
this.changeCategoryAbsolute(0);
|
||||
break;
|
||||
case Key.END:
|
||||
this.changeCategoryAbsolute(this.props.categories.length - 1, -1);
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<nav className="mx_EmojiPicker_header">
|
||||
{this.props.categories.map(category => (
|
||||
<button disabled={!category.enabled} key={category.id} ref={category.ref}
|
||||
className={`mx_EmojiPicker_anchor ${category.visible ? 'mx_EmojiPicker_anchor_visible' : ''}
|
||||
mx_EmojiPicker_anchor_${category.id}`}
|
||||
onClick={() => this.props.onAnchorClick(category.id)} title={category.name} />
|
||||
))}
|
||||
<nav className="mx_EmojiPicker_header" role="tablist" aria-label={_t("Categories")} onKeyDown={this.onKeyDown}>
|
||||
{this.props.categories.map(category => {
|
||||
const classes = classNames(`mx_EmojiPicker_anchor mx_EmojiPicker_anchor_${category.id}`, {
|
||||
mx_EmojiPicker_anchor_visible: category.visible,
|
||||
});
|
||||
// Properties of this button are also modified by EmojiPicker's updateVisibility in DOM.
|
||||
return <button
|
||||
disabled={!category.enabled}
|
||||
key={category.id}
|
||||
ref={category.ref}
|
||||
className={classes}
|
||||
onClick={() => this.props.onAnchorClick(category.id)}
|
||||
title={category.name}
|
||||
role="tab"
|
||||
tabIndex={category.visible ? 0 : -1} // roving
|
||||
aria-selected={category.visible}
|
||||
aria-controls={`mx_EmojiPicker_category_${category.id}`}
|
||||
/>;
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ class QuickReactions extends React.Component {
|
|||
</React.Fragment>
|
||||
}
|
||||
</h2>
|
||||
<ul className="mx_EmojiPicker_list">
|
||||
<ul className="mx_EmojiPicker_list" aria-label={_t("Quick Reactions")}>
|
||||
{QUICK_REACTIONS.map(emoji => <Emoji
|
||||
key={emoji.hexcode} emoji={emoji} onClick={this.props.onClick}
|
||||
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import PropTypes from "prop-types";
|
||||
import EmojiPicker from "./EmojiPicker";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
class ReactionPicker extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -105,6 +106,7 @@ class ReactionPicker extends React.Component {
|
|||
"key": reaction,
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: "message_sent"});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
|||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
export default class ReactionsRowButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -60,6 +61,7 @@ export default class ReactionsRowButton extends React.PureComponent {
|
|||
"key": content,
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: "message_sent"});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -220,10 +220,10 @@ export default class AliasSettings extends React.Component {
|
|||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error creating alias', '', ErrorDialog, {
|
||||
title: _t("Error creating alias"),
|
||||
Modal.createTrackedDialog('Error creating address', '', ErrorDialog, {
|
||||
title: _t("Error creating address"),
|
||||
description: _t(
|
||||
"There was an error creating that alias. It may not be allowed by the server " +
|
||||
"There was an error creating that address. It may not be allowed by the server " +
|
||||
"or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
|
@ -245,15 +245,15 @@ export default class AliasSettings extends React.Component {
|
|||
console.error(err);
|
||||
let description;
|
||||
if (err.errcode === "M_FORBIDDEN") {
|
||||
description = _t("You don't have permission to delete the alias.");
|
||||
description = _t("You don't have permission to delete the address.");
|
||||
} else {
|
||||
description = _t(
|
||||
"There was an error removing that alias. It may no longer exist or a temporary " +
|
||||
"There was an error removing that address. It may no longer exist or a temporary " +
|
||||
"error occurred.",
|
||||
);
|
||||
}
|
||||
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
|
||||
title: _t("Error removing alias"),
|
||||
Modal.createTrackedDialog('Error removing address', '', ErrorDialog, {
|
||||
title: _t("Error removing address"),
|
||||
description,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -190,6 +190,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
const roomId = editedEvent.getRoomId();
|
||||
this._cancelPreviousPendingEdit();
|
||||
this.context.sendMessage(roomId, editContent);
|
||||
dis.dispatch({action: "message_sent"});
|
||||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
|
|
|
@ -403,7 +403,7 @@ export default createReactClass({
|
|||
},
|
||||
|
||||
shouldHighlight: function() {
|
||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent);
|
||||
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
||||
if (!actions || !actions.tweaks) { return false; }
|
||||
|
||||
// don't show self-highlights from another of our clients
|
||||
|
|
|
@ -39,6 +39,14 @@ import * as FormattingUtils from "../../../utils/FormattingUtils";
|
|||
* warning disappears. *
|
||||
*******************************************************************/
|
||||
|
||||
enum NotificationColor {
|
||||
// Inverted (None -> Red) because we do integer comparisons on this
|
||||
None, // nothing special
|
||||
Bold, // no badge, show as unread
|
||||
Grey, // unread notified messages
|
||||
Red, // unread pings
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
|
||||
|
@ -47,16 +55,14 @@ interface IProps {
|
|||
// TODO: Incoming call boxes?
|
||||
}
|
||||
|
||||
interface IBadgeState {
|
||||
showBadge: boolean; // if numUnread > 0 && !showBadge -> bold room
|
||||
numUnread: number; // used only if showBadge or showBadgeHighlight is true
|
||||
hasUnread: number; // used to make the room bold
|
||||
showBadgeHighlight: boolean; // make the badge red
|
||||
isInvite: boolean; // show a `!` instead of a number
|
||||
interface INotificationState {
|
||||
symbol: string;
|
||||
color: NotificationColor;
|
||||
}
|
||||
|
||||
interface IState extends IBadgeState {
|
||||
interface IState {
|
||||
hover: boolean;
|
||||
notificationState: INotificationState;
|
||||
}
|
||||
|
||||
export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||
|
@ -78,8 +84,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
|
||||
this.state = {
|
||||
hover: false,
|
||||
|
||||
...this.getBadgeState(),
|
||||
notificationState: this.getNotificationState(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -87,28 +92,58 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
// TODO: Listen for changes to the badge count and update as needed
|
||||
}
|
||||
|
||||
private updateBadgeCount() {
|
||||
this.setState({...this.getBadgeState()});
|
||||
// XXX: This is a bit of an awful-looking hack. We should probably be using state for
|
||||
// this, but instead we're kinda forced to either duplicate the code or thread a variable
|
||||
// through the code paths. This feels like the least evil option.
|
||||
private get roomIsInvite(): boolean {
|
||||
return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite;
|
||||
}
|
||||
|
||||
private getBadgeState(): IBadgeState {
|
||||
// TODO: Make this code path faster
|
||||
const highlightCount = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight');
|
||||
const numUnread = RoomNotifs.getUnreadNotificationCount(this.props.room);
|
||||
const showBadge = Unread.doesRoomHaveUnreadMessages(this.props.room);
|
||||
const myMembership = getEffectiveMembership(this.props.room.getMyMembership());
|
||||
const isInvite = myMembership === EffectiveMembership.Invite;
|
||||
const notifState = RoomNotifs.getRoomNotifsState(this.props.room.roomId);
|
||||
const shouldShowNotifBadge = RoomNotifs.shouldShowNotifBadge(notifState);
|
||||
const shouldShowHighlightBadge = RoomNotifs.shouldShowMentionBadge(notifState);
|
||||
// TODO: Make use of this function when the notification state needs updating.
|
||||
private updateNotificationState() {
|
||||
this.setState({notificationState: this.getNotificationState()});
|
||||
}
|
||||
|
||||
return {
|
||||
showBadge: (showBadge && shouldShowNotifBadge) || isInvite,
|
||||
numUnread,
|
||||
hasUnread: showBadge,
|
||||
showBadgeHighlight: (highlightCount > 0 && shouldShowHighlightBadge) || isInvite,
|
||||
isInvite,
|
||||
private getNotificationState(): INotificationState {
|
||||
const state: INotificationState = {
|
||||
color: NotificationColor.None,
|
||||
symbol: null,
|
||||
};
|
||||
|
||||
if (this.roomIsInvite) {
|
||||
state.color = NotificationColor.Red;
|
||||
state.symbol = "!";
|
||||
} else {
|
||||
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight');
|
||||
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'total');
|
||||
|
||||
// For a 'true count' we pick the grey notifications first because they include the
|
||||
// red notifications. If we don't have a grey count for some reason we use the red
|
||||
// count. If that count is broken for some reason, assume zero. This avoids us showing
|
||||
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
|
||||
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
|
||||
|
||||
// Note: we only set the symbol if we have an actual count. We don't want to show
|
||||
// zero on badges.
|
||||
|
||||
if (redNotifs > 0) {
|
||||
state.color = NotificationColor.Red;
|
||||
state.symbol = FormattingUtils.formatCount(trueCount);
|
||||
} else if (greyNotifs > 0) {
|
||||
state.color = NotificationColor.Grey;
|
||||
state.symbol = FormattingUtils.formatCount(trueCount);
|
||||
} else {
|
||||
// We don't have any notified messages, but we might have unread messages. Let's
|
||||
// find out.
|
||||
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.props.room);
|
||||
if (hasUnread) {
|
||||
state.color = NotificationColor.Bold;
|
||||
// no symbol for this state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private onTileMouseEnter = () => {
|
||||
|
@ -135,15 +170,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
// TODO: a11y proper
|
||||
// TODO: Render more than bare minimum
|
||||
|
||||
const hasBadge = this.state.notificationState.color > NotificationColor.Bold;
|
||||
const isUnread = this.state.notificationState.color > NotificationColor.None;
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
// 'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_RoomTile_unread': this.state.numUnread > 0 || this.state.hasUnread,
|
||||
'mx_RoomTile_unreadNotify': this.state.showBadge,
|
||||
'mx_RoomTile_highlight': this.state.showBadgeHighlight,
|
||||
'mx_RoomTile_invited': this.state.isInvite,
|
||||
'mx_RoomTile_unread': isUnread,
|
||||
'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey,
|
||||
'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red,
|
||||
'mx_RoomTile_invited': this.roomIsInvite,
|
||||
// 'mx_RoomTile_menuDisplayed': isMenuDisplayed,
|
||||
'mx_RoomTile_noBadges': !this.state.showBadge,
|
||||
'mx_RoomTile_noBadges': !hasBadge,
|
||||
// 'mx_RoomTile_transparent': this.props.transparent,
|
||||
// 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
|
||||
});
|
||||
|
@ -154,13 +191,12 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
|
||||
|
||||
let badge;
|
||||
if (this.state.showBadge) {
|
||||
if (hasBadge) {
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomTile_badge': true,
|
||||
'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed
|
||||
});
|
||||
const formattedCount = this.state.isInvite ? `!` : FormattingUtils.formatCount(this.state.numUnread);
|
||||
badge = <div className={badgeClasses}>{formattedCount}</div>;
|
||||
badge = <div className={badgeClasses}>{this.state.notificationState.symbol}</div>;
|
||||
}
|
||||
|
||||
// TODO: the original RoomTile uses state for the room name. Do we need to?
|
||||
|
@ -170,8 +206,8 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
|||
|
||||
const nameClasses = classNames({
|
||||
'mx_RoomTile_name': true,
|
||||
'mx_RoomTile_invite': this.state.isInvite,
|
||||
'mx_RoomTile_badgeShown': this.state.showBadge,
|
||||
'mx_RoomTile_invite': this.roomIsInvite,
|
||||
'mx_RoomTile_badgeShown': hasBadge,
|
||||
});
|
||||
|
||||
// TODO: Support collapsed state properly
|
||||
|
|
|
@ -312,6 +312,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
event: null,
|
||||
});
|
||||
}
|
||||
dis.dispatch({action: "message_sent"});
|
||||
}
|
||||
|
||||
this.sendHistoryManager.save(this.model);
|
||||
|
|
|
@ -247,7 +247,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
|||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||
<span>
|
||||
{_t("To link to this room, please add an alias.")}
|
||||
{_t("To link to this room, please add an address.")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
|
|||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to subscribe to Mjolnir list', '', ErrorDialog, {
|
||||
title: _t('Error subscribing to list'),
|
||||
description: _t('Please verify the room ID or alias and try again.'),
|
||||
description: _t('Please verify the room ID or address and try again.'),
|
||||
});
|
||||
} finally {
|
||||
this.setState({busy: false});
|
||||
|
@ -305,7 +305,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
|
|||
<form onSubmit={this._onSubscribeList} autoComplete="off">
|
||||
<Field
|
||||
type="text"
|
||||
label={_t("Room ID or alias of ban list")}
|
||||
label={_t("Room ID or address of ban list")}
|
||||
value={this.state.newList}
|
||||
onChange={this._onNewListChanged}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue