Merge branches 'develop' and 't3chguy/confusing_copy' of github.com:matrix-org/matrix-react-sdk into t3chguy/confusing_copy
This commit is contained in:
commit
0713139dc5
53 changed files with 1899 additions and 575 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();
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import { _t } from '../../languageHandler';
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import classNames from "classnames";
|
||||
|
||||
|
@ -50,14 +49,21 @@ export default class ToastContainer extends React.Component {
|
|||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
});
|
||||
const countIndicator = isStacked ? _t(" (1/%(totalCount)s)", {totalCount}) : null;
|
||||
|
||||
let countIndicator;
|
||||
if (isStacked) {
|
||||
countIndicator = `(1/${totalCount})`;
|
||||
}
|
||||
|
||||
const toastProps = Object.assign({}, props, {
|
||||
key,
|
||||
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>);
|
||||
}
|
||||
|
|
|
@ -26,58 +26,48 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import {toPx} from "../../../utils/units";
|
||||
|
||||
const useImageUrl = ({url, urls, idName, name, defaultToInitialLetter}) => {
|
||||
const useImageUrl = ({url, urls}) => {
|
||||
const [imageUrls, setUrls] = useState([]);
|
||||
const [urlsIndex, setIndex] = useState();
|
||||
|
||||
const onError = () => {
|
||||
const nextIndex = urlsIndex + 1;
|
||||
if (nextIndex < imageUrls.length) {
|
||||
// try the next one
|
||||
setIndex(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const defaultImageUrl = useMemo(() => AvatarLogic.defaultAvatarUrlForString(idName || name), [idName, name]);
|
||||
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
|
||||
|
||||
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 = [];
|
||||
if (!SettingsStore.getValue("lowBandwidth")) {
|
||||
_urls = urls || [];
|
||||
_urls = memoizedUrls || [];
|
||||
|
||||
if (url) {
|
||||
_urls.unshift(url); // put in urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultToInitialLetter) {
|
||||
_urls.push(defaultImageUrl); // lowest priority
|
||||
}
|
||||
|
||||
// deduplicate URLs
|
||||
_urls = Array.from(new Set(_urls));
|
||||
|
||||
setIndex(0);
|
||||
setUrls(_urls);
|
||||
}, [url, ...(urls || [])]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
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 && urlsIndex > 0 ) { // Did we fall back?
|
||||
// Start from the highest priority URL again
|
||||
if (reconnected) {
|
||||
setIndex(0);
|
||||
}
|
||||
}, [urlsIndex]);
|
||||
}, []);
|
||||
useEventEmitter(cli, "sync", onClientSync);
|
||||
|
||||
const imageUrl = imageUrls[urlsIndex];
|
||||
return [imageUrl, imageUrl === defaultImageUrl, onError];
|
||||
return [imageUrl, onError];
|
||||
};
|
||||
|
||||
const BaseAvatar = (props) => {
|
||||
|
@ -96,9 +86,9 @@ const BaseAvatar = (props) => {
|
|||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [imageUrl, isDefault, onError] = useImageUrl({url, urls, idName, name, defaultToInitialLetter});
|
||||
const [imageUrl, onError] = useImageUrl({url, urls});
|
||||
|
||||
if (isDefault) {
|
||||
if (!imageUrl && defaultToInitialLetter) {
|
||||
const initialLetter = AvatarLogic.getInitialLetter(name);
|
||||
const textNode = (
|
||||
<span
|
||||
|
@ -116,7 +106,7 @@ const BaseAvatar = (props) => {
|
|||
const imgNode = (
|
||||
<img
|
||||
className="mx_BaseAvatar_image"
|
||||
src={imageUrl}
|
||||
src={AvatarLogic.defaultAvatarUrlForString(idName || name)}
|
||||
alt=""
|
||||
title={title}
|
||||
onError={onError}
|
||||
|
|
|
@ -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