Merge branch 'develop' into travis/mjolnir

This commit is contained in:
Travis Ralston 2019-11-12 10:00:01 -07:00 committed by GitHub
commit 6d0b388fa2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 2146 additions and 304 deletions

View file

@ -39,23 +39,10 @@ const FilePanel = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this.updateTimelineSet(this.props.roomId);
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.roomId !== this.props.roomId) {
// otherwise we race between re-rendering the TimelinePanel and setting the new timelineSet.
//
// FIXME: this race only happens because of the promise returned by getOrCreateFilter().
// We should only need to create the containsUrl filter once per login session, so in practice
// it shouldn't be being done here at all. Then we could just update the timelineSet directly
// without resetting it first, and speed up room-change.
this.setState({ timelineSet: null });
this.updateTimelineSet(nextProps.roomId);
}
},
updateTimelineSet: function(roomId) {
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);

View file

@ -60,6 +60,7 @@ import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import { setTheme } from "../../theme";
import { storeRoomAliasInCache } from '../../RoomAliasCache';
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
@ -866,7 +867,12 @@ export default createReactClass({
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
if (room) {
const theAlias = Rooms.getDisplayAliasForRoom(room);
if (theAlias) presentedId = theAlias;
if (theAlias) {
presentedId = theAlias;
// Store display alias of the presented room in cache to speed future
// navigation.
storeRoomAliasInCache(theAlias, room.roomId);
}
// Store this as the ID of the last room accessed. This is so that we can
// persist which room is being stored across refreshes and browser quits.

View file

@ -67,6 +67,9 @@ const RoomSubList = createReactClass({
// some values to get LazyRenderList starting
scrollerHeight: 800,
scrollTop: 0,
// React 16's getDerivedStateFromProps(props, state) doesn't give the previous props so
// we have to store the length of the list here so we can see if it's changed or not...
listLength: null,
};
},
@ -79,11 +82,20 @@ const RoomSubList = createReactClass({
};
},
componentWillMount: function() {
componentDidMount: function() {
this._headerButton = createRef();
this.dispatcherRef = dis.register(this.onAction);
},
statics: {
getDerivedStateFromProps: function(props, state) {
return {
listLength: props.list.length,
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
};
},
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},

View file

@ -759,8 +759,10 @@ module.exports = createReactClass({
_getMessagesHeight() {
const itemlist = this.refs.itemlist;
const lastNode = itemlist.lastElementChild;
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
// 18 is itemlist padding
return (lastNode.offsetTop + lastNode.clientHeight) - itemlist.firstElementChild.offsetTop + (18 * 2);
return lastNodeBottom - firstNodeTop + (18 * 2);
},
_topFromBottom(node) {

View file

@ -24,6 +24,10 @@ import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import { _t } from '../../../languageHandler';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import DMRoomMap from '../../../utils/DMRoomMap';
import createRoom from "../../../createRoom";
import dis from "../../../dispatcher";
import SettingsStore from '../../../settings/SettingsStore';
const MODE_LEGACY = 'legacy';
const MODE_SAS = 'sas';
@ -86,25 +90,37 @@ export default class DeviceVerifyDialog extends React.Component {
this.props.onFinished(confirm);
}
_onSasRequestClick = () => {
_onSasRequestClick = async () => {
this.setState({
phase: PHASE_WAIT_FOR_PARTNER_TO_ACCEPT,
});
this._verifier = MatrixClientPeg.get().beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
this._verifier.on('show_sas', this._onVerifierShowSas);
this._verifier.verify().then(() => {
const client = MatrixClientPeg.get();
const verifyingOwnDevice = this.props.userId === client.getUserId();
try {
if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) {
const roomId = await ensureDMExistsAndOpen(this.props.userId);
// throws upon cancellation before having started
this._verifier = await client.requestVerificationDM(
this.props.userId, roomId, [verificationMethods.SAS],
);
} else {
this._verifier = client.beginKeyVerification(
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
);
}
this._verifier.on('show_sas', this._onVerifierShowSas);
// throws upon cancellation
await this._verifier.verify();
this.setState({phase: PHASE_VERIFIED});
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
this._verifier = null;
}).catch((e) => {
} catch (e) {
console.log("Verification failed", e);
this.setState({
phase: PHASE_CANCELLED,
});
this._verifier = null;
});
}
}
_onSasMatchesClick = () => {
@ -299,3 +315,30 @@ export default class DeviceVerifyDialog extends React.Component {
}
}
async function ensureDMExistsAndOpen(userId) {
const client = MatrixClientPeg.get();
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
const rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
if (r && r.getMyMembership() === "join") {
const member = r.getMember(userId);
return member && (member.membership === "invite" || member.membership === "join");
}
return false;
});
let roomId;
if (suitableDMRooms.length) {
const room = suitableDMRooms[0];
roomId = room.roomId;
} else {
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
}
// don't use andView and spinner in createRoom, together, they cause this dialog to close and reopen,
// we causes us to loose the verifier and restart, and we end up having two verification requests
dis.dispatch({
action: 'view_room',
room_id: roomId,
should_peek: false,
});
return roomId;
}

View file

@ -116,7 +116,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent {
nodes.push((
<EditHistoryMessage
key={e.getId()}
previousEdit={!isBaseEvent && allEvents[i + 1]}
previousEdit={!isBaseEvent ? allEvents[i + 1] : null}
isBaseEvent={isBaseEvent}
mxEvent={e}
isTwelveHour={this.state.isTwelveHour}

View file

@ -1,6 +1,7 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -23,6 +24,7 @@ import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import escapeHtml from "escape-html";
// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
@ -101,6 +103,15 @@ export default class ReplyThread extends React.Component {
if (html) html = this.stripHTMLReply(html);
}
if (!body) body = ""; // Always ensure we have a body, for reasons.
// Escape the body to use as HTML below.
// We also run a nl2br over the result to fix the fallback representation. We do this
// after converting the text to safe HTML to avoid user-provided BR's from being converted.
if (!html) html = escapeHtml(body).replace(/\n/g, '<br/>');
// dev note: do not rely on `body` being safe for HTML usage below.
const evLink = permalinkCreator.forEvent(ev.getId());
const userLink = makeUserPermalink(ev.getSender());
const mxid = ev.getSender();
@ -110,7 +121,7 @@ export default class ReplyThread extends React.Component {
case 'm.text':
case 'm.notice': {
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
+ `<br>${html || body}</blockquote></mx-reply>`;
+ `<br>${html}</blockquote></mx-reply>`;
const lines = body.trim().split('\n');
if (lines.length > 0) {
lines[0] = `<${mxid}> ${lines[0]}`;
@ -140,7 +151,7 @@ export default class ReplyThread extends React.Component {
break;
case 'm.emote': {
html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> * `
+ `<a href="${userLink}">${mxid}</a><br>${html || body}</blockquote></mx-reply>`;
+ `<a href="${userLink}">${mxid}</a><br>${html}</blockquote></mx-reply>`;
const lines = body.trim().split('\n');
if (lines.length > 0) {
lines[0] = `* <${mxid}> ${lines[0]}`;

View file

@ -48,7 +48,14 @@ const DATA_BY_CATEGORY = {
};
const DATA_BY_EMOJI = {};
const VARIATION_SELECTOR = String.fromCharCode(0xFE0F);
EMOJIBASE.forEach(emoji => {
if (emoji.unicode.includes(VARIATION_SELECTOR)) {
// Clone data into variation-less version
emoji = Object.assign({}, emoji, {
unicode: emoji.unicode.replace(VARIATION_SELECTOR, ""),
});
}
DATA_BY_EMOJI[emoji.unicode] = emoji;
const categoryId = EMOJIBASE_CATEGORY_IDS[emoji.group];
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
@ -82,7 +89,10 @@ class EmojiPicker extends React.Component {
viewportHeight: 280,
};
this.recentlyUsed = recent.get().map(unicode => DATA_BY_EMOJI[unicode]);
// Convert recent emoji characters to emoji data, removing unknowns.
this.recentlyUsed = recent.get()
.map(unicode => DATA_BY_EMOJI[unicode])
.filter(data => !!data);
this.memoizedDataByCategory = {
recent: this.recentlyUsed,
...DATA_BY_CATEGORY,

View file

@ -16,17 +16,19 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import EMOJIBASE from 'emojibase-data/en/compact.json';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { findEmojiData } from '../../../HtmlUtils';
const QUICK_REACTIONS = ["👍️", "👎️", "😄", "🎉", "😕", "❤️", "🚀", "👀"];
EMOJIBASE.forEach(emoji => {
const index = QUICK_REACTIONS.indexOf(emoji.unicode);
if (index !== -1) {
QUICK_REACTIONS[index] = emoji;
const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => {
const data = findEmojiData(emoji);
if (!data) {
throw new Error(`Emoji ${emoji} doesn't exist in emojibase`);
}
// Prefer our unicode value for quick reactions (which does not have
// variation selectors).
return Object.assign({}, data, { unicode: emoji });
});
class QuickReactions extends React.Component {

View file

@ -57,7 +57,8 @@ export default class DateSeparator extends React.Component {
render() {
// ARIA treats <hr/>s as separators, here we abuse them slightly so manually treat this entire thing as one
return <h2 className="mx_DateSeparator" role="separator">
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return <h2 className="mx_DateSeparator" role="separator" tabIndex={-1}>
<hr role="none" />
<div>{ this.getLabel() }</div>
<hr role="none" />

View file

@ -0,0 +1,132 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver';
export default class MKeyVerificationConclusion extends React.Component {
constructor(props) {
super(props);
this.keyVerificationState = null;
this.state = {
done: false,
cancelled: false,
otherPartyUserId: null,
cancelPartyUserId: null,
};
const rel = this.props.mxEvent.getRelation();
if (rel) {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.mxEvent.getRoomId());
const requestEvent = room.findEventById(rel.event_id);
if (requestEvent) {
this._createStateObserver(requestEvent, client);
this.state = this._copyState();
} else {
const findEvent = event => {
if (event.getId() === rel.event_id) {
this._createStateObserver(event, client);
this.setState(this._copyState());
room.removeListener("Room.timeline", findEvent);
}
};
room.on("Room.timeline", findEvent);
}
}
}
_createStateObserver(requestEvent, client) {
this.keyVerificationState = new KeyVerificationStateObserver(requestEvent, client, () => {
this.setState(this._copyState());
});
}
_copyState() {
const {done, cancelled, otherPartyUserId, cancelPartyUserId} = this.keyVerificationState;
return {done, cancelled, otherPartyUserId, cancelPartyUserId};
}
componentDidMount() {
if (this.keyVerificationState) {
this.keyVerificationState.attach();
}
}
componentWillUnmount() {
if (this.keyVerificationState) {
this.keyVerificationState.detach();
}
}
_getName(userId) {
const roomId = this.props.mxEvent.getRoomId();
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
const member = room.getMember(userId);
return member ? member.name : userId;
}
_userLabel(userId) {
const name = this._getName(userId);
if (name !== userId) {
return _t("%(name)s (%(userId)s)", {name, userId});
} else {
return userId;
}
}
render() {
const {mxEvent} = this.props;
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
let title;
if (this.state.done) {
title = _t("You verified %(name)s", {name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
} else if (this.state.cancelled) {
if (mxEvent.getSender() === myUserId) {
title = _t("You cancelled verifying %(name)s",
{name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
} else if (mxEvent.getSender() === this.state.otherPartyUserId) {
title = _t("%(name)s cancelled verifying",
{name: getNameForEventRoom(this.state.otherPartyUserId, mxEvent)});
}
}
if (title) {
const subtitle = userLabelForEventRoom(this.state.otherPartyUserId, mxEvent);
const classes = classNames("mx_EventTile_bubble", "mx_KeyVerification", "mx_KeyVerification_icon", {
mx_KeyVerification_icon_verified: this.state.done,
});
return (<div className={classes}>
<div className="mx_KeyVerification_title">{title}</div>
<div className="mx_KeyVerification_subtitle">{subtitle}</div>
</div>);
}
return null;
}
}
MKeyVerificationConclusion.propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};

View file

@ -0,0 +1,141 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import sdk from '../../../index';
import Modal from "../../../Modal";
import { _t } from '../../../languageHandler';
import KeyVerificationStateObserver, {getNameForEventRoom, userLabelForEventRoom}
from '../../../utils/KeyVerificationStateObserver';
export default class MKeyVerificationRequest extends React.Component {
constructor(props) {
super(props);
this.keyVerificationState = new KeyVerificationStateObserver(this.props.mxEvent, MatrixClientPeg.get(), () => {
this.setState(this._copyState());
});
this.state = this._copyState();
}
_copyState() {
const {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId} = this.keyVerificationState;
return {accepted, done, cancelled, cancelPartyUserId, otherPartyUserId};
}
componentDidMount() {
this.keyVerificationState.attach();
}
componentWillUnmount() {
this.keyVerificationState.detach();
}
_onAcceptClicked = () => {
const IncomingSasDialog = sdk.getComponent('views.dialogs.IncomingSasDialog');
// todo: validate event, for example if it has sas in the methods.
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS);
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
verifier,
});
};
_onRejectClicked = () => {
// todo: validate event, for example if it has sas in the methods.
const verifier = MatrixClientPeg.get().acceptVerificationDM(this.props.mxEvent, verificationMethods.SAS);
verifier.cancel("User declined");
};
_acceptedLabel(userId) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
if (userId === myUserId) {
return _t("You accepted");
} else {
return _t("%(name)s accepted", {name: getNameForEventRoom(userId, this.props.mxEvent)});
}
}
_cancelledLabel(userId) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
if (userId === myUserId) {
return _t("You cancelled");
} else {
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent)});
}
}
render() {
const {mxEvent} = this.props;
const fromUserId = mxEvent.getSender();
const content = mxEvent.getContent();
const toUserId = content.to;
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
const isOwn = fromUserId === myUserId;
let title;
let subtitle;
let stateNode;
if (this.state.accepted || this.state.cancelled) {
let stateLabel;
if (this.state.accepted) {
stateLabel = this._acceptedLabel(toUserId);
} else if (this.state.cancelled) {
stateLabel = this._cancelledLabel(this.state.cancelPartyUserId);
}
stateNode = (<div className="mx_KeyVerification_state">{stateLabel}</div>);
}
if (toUserId === myUserId) { // request sent to us
title = (<div className="mx_KeyVerification_title">{
_t("%(name)s wants to verify", {name: getNameForEventRoom(fromUserId, mxEvent)})}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(fromUserId, mxEvent)}</div>);
const isResolved = !(this.state.accepted || this.state.cancelled || this.state.done);
if (isResolved) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
stateNode = (<div className="mx_KeyVerification_buttons">
<AccessibleButton kind="decline" onClick={this._onRejectClicked}>{_t("Decline")}</AccessibleButton>
<AccessibleButton kind="accept" onClick={this._onAcceptClicked}>{_t("Accept")}</AccessibleButton>
</div>);
}
} else if (isOwn) { // request sent by us
title = (<div className="mx_KeyVerification_title">{
_t("You sent a verification request")}</div>);
subtitle = (<div className="mx_KeyVerification_subtitle">{
userLabelForEventRoom(this.state.otherPartyUserId, mxEvent)}</div>);
}
if (title) {
return (<div className="mx_EventTile_bubble mx_KeyVerification mx_KeyVerification_icon">
{title}
{subtitle}
{stateNode}
</div>);
}
return null;
}
}
MKeyVerificationRequest.propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
};

View file

@ -180,7 +180,8 @@ export default class MessageActionBar extends React.PureComponent {
/>;
}
return <div className="mx_MessageActionBar">
// aria-live=off to not have this read out automatically as navigating around timeline, gets repetitive.
return <div className="mx_MessageActionBar" role="toolbar" aria-label={_t("Message Actions")} aria-live="off">
{reactButton}
{replyButton}
{editButton}

View file

@ -28,7 +28,7 @@ export default class MessageTimestamp extends React.Component {
render() {
const date = new Date(this.props.ts);
return (
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)}>
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}>
{ formatTime(date, this.props.showTwelveHour) }
</span>
);

View file

@ -566,6 +566,7 @@ export default class BasicMessageEditor extends React.Component {
aria-haspopup="listbox"
aria-expanded={Boolean(this.state.autoComplete)}
aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined}
dir="auto"
/>
</div>);
}

View file

@ -32,12 +32,16 @@ const TextForEvent = require('../../../TextForEvent');
import dis from '../../../dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import {EventStatus, MatrixClient} from 'matrix-js-sdk';
import {formatTime} from "../../../DateUtils";
import MatrixClientPeg from '../../../MatrixClientPeg';
const ObjectUtils = require('../../../ObjectUtils');
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
'm.sticker': 'messages.MessageEvent',
'm.key.verification.cancel': 'messages.MKeyVerificationConclusion',
'm.key.verification.done': 'messages.MKeyVerificationConclusion',
'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent',
@ -67,6 +71,31 @@ const stateEventTileTypes = {
function getHandlerTile(ev) {
const type = ev.getType();
// don't show verification requests we're not involved in,
// not even when showing hidden events
if (type === "m.room.message") {
const content = ev.getContent();
if (content && content.msgtype === "m.key.verification.request") {
const client = MatrixClientPeg.get();
const me = client && client.getUserId();
if (ev.getSender() !== me && content.to !== me) {
return undefined;
} else {
return "messages.MKeyVerificationRequest";
}
}
}
// these events are sent by both parties during verification, but we only want to render one
// tile once the verification concludes, so filter out the one from the other party.
if (type === "m.key.verification.done") {
const client = MatrixClientPeg.get();
const me = client && client.getUserId();
if (ev.getSender() !== me) {
return undefined;
}
}
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
}
@ -526,8 +555,11 @@ module.exports = createReactClass({
const eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a room
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification"));
let isInfoMessage = (
eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create'
!isBubbleMessage && eventType !== 'm.room.message' &&
eventType !== 'm.sticker' && eventType !== 'm.room.create'
);
let tileHandler = getHandlerTile(this.props.mxEvent);
@ -560,6 +592,7 @@ module.exports = createReactClass({
const isEditing = !!this.props.editState;
const classes = classNames({
mx_EventTile_bubbleContainer: isBubbleMessage,
mx_EventTile: true,
mx_EventTile_isEditing: isEditing,
mx_EventTile_info: isInfoMessage,
@ -595,7 +628,7 @@ module.exports = createReactClass({
if (this.props.tileShape === "notif") {
avatarSize = 24;
needsSenderProfile = true;
} else if (tileHandler === 'messages.RoomCreate') {
} else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) {
avatarSize = 0;
needsSenderProfile = false;
} else if (isInfoMessage) {
@ -786,14 +819,19 @@ module.exports = createReactClass({
this.props.permalinkCreator,
'replyThread',
);
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
<div className={classes}>
<div className={classes} tabIndex={-1}>
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{ sender }
<div className="mx_EventTile_line">
<a href={permalink} onClick={this.onPermalinkClicked}>
<div className={classNames("mx_EventTile_line", {mx_EventTile_bubbleLine: isBubbleMessage})}>
<a
href={permalink}
onClick={this.onPermalinkClicked}
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
>
{ timestamp }
</a>
{ this._renderE2EPadlock() }

View file

@ -353,7 +353,6 @@ export default class RoomBreadcrumbs extends React.Component {
onMouseEnter={() => this._onMouseEnter(r.room)}
onMouseLeave={() => this._onMouseLeave(r.room)}
aria-label={_t("Room %(name)s", {name: r.room.name})}
role="listitem"
>
<RoomAvatar room={r.room} width={32} height={32} />
{badge}
@ -363,7 +362,7 @@ export default class RoomBreadcrumbs extends React.Component {
);
});
return (
<div role="list" aria-orientation="horizontal" aria-roledescription={_t("Recent rooms")}>
<div role="toolbar" aria-label={_t("Recent rooms")}>
<IndicatorScrollbar
ref="scroller"
className="mx_RoomBreadcrumbs"

View file

@ -24,9 +24,8 @@ import Modal from '../../../Modal';
import dis from "../../../dispatcher";
import { getThreepidsWithBindStatus } from '../../../boundThreepids';
import IdentityAuthClient from "../../../IdentityAuthClient";
import {SERVICE_TYPES} from "matrix-js-sdk";
import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl } from '../../../utils/IdentityServerUtils';
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
// We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms
@ -162,19 +161,8 @@ export default class SetIdServer extends React.Component {
let save = true;
// Double check that the identity server even has terms of service.
let terms;
try {
terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl);
} catch (e) {
console.error(e);
if (e.cors === "rejected" || e.httpStatus === 404) {
terms = null;
} else {
throw e;
}
}
if (!terms || !terms["policies"] || Object.keys(terms["policies"]).length <= 0) {
const hasTerms = await doesIdentityServerHaveTerms(fullUrl);
if (!hasTerms) {
const [confirmed] = await this._showNoTermsWarning(fullUrl);
save = confirmed;
}