Merge remote-tracking branch 'upstream/develop' into feature-surround-with
This commit is contained in:
commit
b99e39e011
383 changed files with 13499 additions and 6309 deletions
|
@ -44,6 +44,7 @@ import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
|||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import CallEventGrouper from "../../structures/CallEventGrouper";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
|
@ -60,10 +61,7 @@ const eventTileTypes = {
|
|||
[EventType.Sticker]: 'messages.MessageEvent',
|
||||
[EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
|
||||
[EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
|
||||
[EventType.CallInvite]: 'messages.TextualEvent',
|
||||
[EventType.CallAnswer]: 'messages.TextualEvent',
|
||||
[EventType.CallHangup]: 'messages.TextualEvent',
|
||||
[EventType.CallReject]: 'messages.TextualEvent',
|
||||
[EventType.CallInvite]: 'messages.CallEvent',
|
||||
};
|
||||
|
||||
const stateEventTileTypes = {
|
||||
|
@ -290,6 +288,9 @@ interface IProps {
|
|||
// Helper to build permalinks for the room
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
|
||||
// CallEventGrouper for this event
|
||||
callEventGrouper?: CallEventGrouper;
|
||||
|
||||
// Symbol of the root node
|
||||
as?: string;
|
||||
|
||||
|
@ -710,9 +711,12 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
|
||||
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
||||
avatars.unshift(
|
||||
<ReadReceiptMarker key={userId} member={receipt.roomMember}
|
||||
<ReadReceiptMarker
|
||||
key={userId}
|
||||
member={receipt.roomMember}
|
||||
fallbackUserId={userId}
|
||||
leftOffset={left} hidden={hidden}
|
||||
leftOffset={left}
|
||||
hidden={hidden}
|
||||
readReceiptInfo={readReceiptInfo}
|
||||
checkUnmounting={this.props.checkUnmounting}
|
||||
suppressAnimation={this.suppressReadReceiptAnimation}
|
||||
|
@ -892,6 +896,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
|
||||
mx_EventTile_bad: isEncryptionFailure,
|
||||
mx_EventTile_emote: msgtype === 'm.emote',
|
||||
mx_EventTile_noSender: this.props.hideSender,
|
||||
});
|
||||
|
||||
// If the tile is in the Sending state, don't speak the message.
|
||||
|
@ -948,8 +953,10 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
avatar = (
|
||||
<div className="mx_EventTile_avatar">
|
||||
<MemberAvatar member={member}
|
||||
width={avatarSize} height={avatarSize}
|
||||
<MemberAvatar
|
||||
member={member}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
viewUserOnClick={true}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1141,6 +1148,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
{ ircTimestamp }
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
{ avatar }
|
||||
<div className="mx_EventTile_line" key="mx_EventTile_line">
|
||||
{ groupTimestamp }
|
||||
{ groupPadlock }
|
||||
|
@ -1154,13 +1162,14 @@ export default class EventTile extends React.Component<IProps, IState> {
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
callEventGrouper={this.props.callEventGrouper}
|
||||
/>
|
||||
{ keyRequestInfo }
|
||||
{ actionBar }
|
||||
{ this.props.layout === Layout.IRC && (reactionsRow) }
|
||||
</div>
|
||||
{ reactionsRow }
|
||||
{ this.props.layout !== Layout.IRC && (reactionsRow) }
|
||||
{ msgOption }
|
||||
{ avatar }
|
||||
</>)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,10 +28,11 @@ export default (props) => {
|
|||
badge = (<div className="mx_JumpToBottomButton_badge">{ props.numUnreadMessages }</div>);
|
||||
}
|
||||
return (<div className={className}>
|
||||
<AccessibleButton className="mx_JumpToBottomButton_scrollDown"
|
||||
<AccessibleButton
|
||||
className="mx_JumpToBottomButton_scrollDown"
|
||||
title={_t("Scroll to most recent messages")}
|
||||
onClick={props.onScrollToBottomClick}>
|
||||
</AccessibleButton>
|
||||
onClick={props.onScrollToBottomClick}
|
||||
/>
|
||||
{ badge }
|
||||
</div>);
|
||||
};
|
||||
|
|
|
@ -306,10 +306,16 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
// For now we'll pretend this is any entity. It should probably be a separate tile.
|
||||
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||
return (
|
||||
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
} name={text} presenceState="online" suppressOnHover={true}
|
||||
onClick={onClick} />
|
||||
<EntityTile
|
||||
className="mx_EntityTile_ellipsis"
|
||||
avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||
}
|
||||
name={text}
|
||||
presenceState="online"
|
||||
suppressOnHover={true}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -465,8 +471,12 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
|
||||
} else {
|
||||
// Is a 3pid invite
|
||||
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
|
||||
onClick={() => this.onPending3pidInviteClick(m)} />;
|
||||
return <EntityTile
|
||||
key={m.getStateKey()}
|
||||
name={m.getContent().display_name}
|
||||
suppressOnHover={true}
|
||||
onClick={() => this.onPending3pidInviteClick(m)}
|
||||
/>;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
|||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
||||
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
||||
import { RecordingState } from "../../../voice/VoiceRecording";
|
||||
import { RecordingState } from "../../../audio/VoiceRecording";
|
||||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
|
@ -98,9 +98,7 @@ const EmojiButton = ({ addEmoji }) => {
|
|||
isExpanded={menuDisplayed}
|
||||
title={_t('Emoji picker')}
|
||||
inputRef={button}
|
||||
>
|
||||
|
||||
</ContextMenuTooltipButton>
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</React.Fragment>;
|
||||
|
@ -344,8 +342,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
private onVoiceStoreUpdate = () => {
|
||||
const recording = VoiceRecordingStore.instance.activeRecording;
|
||||
this.setState({ haveRecording: !!recording });
|
||||
if (recording) {
|
||||
// Delay saying we have a recording until it is started, as we might not yet have A/V permissions
|
||||
recording.on(RecordingState.Started, () => {
|
||||
this.setState({ haveRecording: !!VoiceRecordingStore.instance.activeRecording });
|
||||
});
|
||||
// We show a little heads up that the recording is about to automatically end soon. The 3s
|
||||
// display time is completely arbitrary. Note that we don't need to deregister the listener
|
||||
// because the recording instance will clean that up for us.
|
||||
|
@ -353,6 +354,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
this.setState({ recordingTimeLeftSeconds: secondsLeft });
|
||||
setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
|
||||
});
|
||||
} else {
|
||||
this.setState({ haveRecording: false });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -391,12 +394,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue("feature_voice_messages")) {
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={c => this.voiceRecordingButton = c}
|
||||
room={this.props.room} />);
|
||||
}
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={c => this.voiceRecordingButton = c}
|
||||
room={this.props.room} />);
|
||||
|
||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||
controls.push(
|
||||
|
@ -439,7 +440,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
|||
if (secondsLeft) {
|
||||
recordingTooltip = <Tooltip
|
||||
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
|
||||
alignment={Alignment.Top} yOffset={-50}
|
||||
alignment={Alignment.Top}
|
||||
yOffset={-50}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
|
|
@ -58,13 +58,18 @@ const NewRoomIntro = () => {
|
|||
const member = room?.getMember(dmPartner);
|
||||
const displayName = member?.rawDisplayName || dmPartner;
|
||||
body = <React.Fragment>
|
||||
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} onClick={() => {
|
||||
defaultDispatcher.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || { userId: dmPartner } as User,
|
||||
});
|
||||
}} />
|
||||
<RoomAvatar
|
||||
room={room}
|
||||
width={AVATAR_SIZE}
|
||||
height={AVATAR_SIZE}
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || { userId: dmPartner } as User,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<h2>{ room.name }</h2>
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ export default class PinnedEventTile extends React.Component<IProps> {
|
|||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent
|
||||
mxEvent={this.props.event}
|
||||
// @ts-ignore - complaining that className is invalid when it's not
|
||||
className="mx_PinnedEventTile_body"
|
||||
maxImageHeight={150}
|
||||
onHeightChanged={() => {}} // we need to give this, apparently
|
||||
|
|
|
@ -145,7 +145,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
if (oldInfo && oldInfo.left) {
|
||||
// start at the old height and in the old h pos
|
||||
startStyles.push({ top: startTopOffset+"px",
|
||||
left: toPx(oldInfo.left) });
|
||||
left: toPx(oldInfo.left) });
|
||||
}
|
||||
|
||||
startStyles.push({ top: startTopOffset+'px', left: '0' });
|
||||
|
@ -174,14 +174,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
title = _t(
|
||||
"Seen by %(userName)s at %(dateTime)s",
|
||||
{ userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
dateTime: dateString },
|
||||
);
|
||||
} else {
|
||||
title = _t(
|
||||
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
|
||||
{ displayName: this.props.member.rawDisplayName,
|
||||
userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
userName: this.props.fallbackUserId,
|
||||
dateTime: dateString },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -192,7 +192,9 @@ export default class ReadReceiptMarker extends React.PureComponent {
|
|||
member={this.props.member}
|
||||
fallbackUserId={this.props.fallbackUserId}
|
||||
aria-hidden="true"
|
||||
width={14} height={14} resizeMethod="crop"
|
||||
width={14}
|
||||
height={14}
|
||||
resizeMethod="crop"
|
||||
style={style}
|
||||
title={title}
|
||||
onClick={this.props.onClick}
|
||||
|
|
|
@ -67,15 +67,21 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
};
|
||||
|
||||
private onClick = (e: React.MouseEvent): void => {
|
||||
// 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.
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
const clickTarget = e.target as HTMLElement;
|
||||
// Following a link within a reply should not dispatch the `view_room` action
|
||||
// so that the browser can direct the user to the correct location
|
||||
// The exception being the link wrapping the reply
|
||||
if (clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null) {
|
||||
// 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.
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
event_id: this.props.mxEvent.getId(),
|
||||
highlighted: true,
|
||||
room_id: this.props.mxEvent.getRoomId(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
|
|
|
@ -105,7 +105,9 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
|
||||
return (
|
||||
<CSSTransition
|
||||
appear={true} in={this.state.doAnimation} timeout={640}
|
||||
appear={true}
|
||||
in={this.state.doAnimation}
|
||||
timeout={640}
|
||||
classNames='mx_RoomBreadcrumbs'
|
||||
>
|
||||
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
|
||||
|
|
|
@ -105,8 +105,12 @@ export default class RoomDetailRow extends React.Component {
|
|||
|
||||
return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
|
||||
<td className="mx_RoomDirectory_roomAvatar">
|
||||
<BaseAvatar width={24} height={24} resizeMethod='crop'
|
||||
name={name} idName={name}
|
||||
<BaseAvatar
|
||||
width={24}
|
||||
height={24}
|
||||
resizeMethod='crop'
|
||||
name={name}
|
||||
idName={name}
|
||||
url={avatarUrl} />
|
||||
</td>
|
||||
<td className="mx_RoomDirectory_roomDescription">
|
||||
|
|
|
@ -29,6 +29,8 @@ import RoomTopic from "../elements/RoomTopic";
|
|||
import RoomName from "../elements/RoomName";
|
||||
import { PlaceCallType } from "../../../CallHandler";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import Modal from '../../../Modal';
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { throttle } from 'lodash';
|
||||
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
|
@ -87,6 +89,14 @@ export default class RoomHeader extends React.Component<IProps> {
|
|||
this.forceUpdate();
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
private displayInfoDialogAboutScreensharing() {
|
||||
Modal.createDialog(InfoDialog, {
|
||||
title: _t("Screen sharing is here!"),
|
||||
description: _t("You can now share your screen by pressing the \"screen share\" " +
|
||||
"button during a call. You can even do this in audio calls if both sides support it!"),
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
let searchStatus = null;
|
||||
|
||||
|
@ -185,8 +195,8 @@ export default class RoomHeader extends React.Component<IProps> {
|
|||
videoCallButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
||||
onClick={(ev) => this.props.onCallPlaced(
|
||||
ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)}
|
||||
onClick={(ev) => ev.shiftKey ?
|
||||
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
|
||||
title={_t("Video call")} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ interface IState {
|
|||
suggestedRooms: ISuggestedRoom[];
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
export const TAG_ORDER: TagID[] = [
|
||||
DefaultTagID.Invite,
|
||||
DefaultTagID.Favourite,
|
||||
DefaultTagID.DM,
|
||||
|
@ -140,7 +140,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
||||
showCreateNewRoom(SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
|
@ -153,7 +153,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
||||
showAddExistingRooms(SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
|
@ -428,7 +428,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
groupId={g.groupId}
|
||||
groupName={g.name}
|
||||
groupAvatarUrl={g.avatarUrl}
|
||||
width={32} height={32} resizeMethod='crop'
|
||||
width={32}
|
||||
height={32}
|
||||
resizeMethod='crop'
|
||||
/>
|
||||
);
|
||||
const openGroup = () => {
|
||||
|
|
|
@ -536,8 +536,10 @@ export default class RoomPreviewBar extends React.Component {
|
|||
"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/element-web/issues/new/choose"
|
||||
target="_blank" rel="noreferrer noopener">{ label }</a> },
|
||||
{ issueLink: label => <a
|
||||
href="https://github.com/vector-im/element-web/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{ label }</a> },
|
||||
),
|
||||
];
|
||||
break;
|
||||
|
|
|
@ -419,7 +419,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuRadio
|
||||
label={_t("Global")}
|
||||
label={_t("Use default")}
|
||||
active={state === ALL_MESSAGES}
|
||||
iconClassName="mx_RoomTile_iconBell"
|
||||
onClick={this.onClickAllNotifs}
|
||||
|
|
|
@ -514,13 +514,11 @@ export default class SendMessageComposer extends React.Component<IProps> {
|
|||
|
||||
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
|
||||
const { clipboardData } = event;
|
||||
// Prioritize text on the clipboard over files as Office on macOS puts a bitmap
|
||||
// in the clipboard as well as the content being copied.
|
||||
if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) {
|
||||
// This actually not so much for 'files' as such (at time of writing
|
||||
// neither chrome nor firefox let you paste a plain file copied
|
||||
// from Finder) but more images copied from a different website
|
||||
// / word processor etc.
|
||||
// Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap
|
||||
// in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore.
|
||||
// We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer
|
||||
// it puts the filename in as text/plain which we want to ignore.
|
||||
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
|
||||
ContentMessages.sharedInstance().sendContentListToRoom(
|
||||
Array.from(clipboardData.files), this.props.room.roomId, this.context,
|
||||
);
|
||||
|
|
|
@ -35,13 +35,15 @@ export default class SimpleRoomHeader extends React.Component {
|
|||
let icon;
|
||||
if (this.props.icon) {
|
||||
icon = <img
|
||||
className="mx_RoomHeader_icon" src={this.props.icon}
|
||||
width="25" height="25"
|
||||
className="mx_RoomHeader_icon"
|
||||
src={this.props.icon}
|
||||
width="25"
|
||||
height="25"
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
|
||||
<div className="mx_RoomHeader mx_RoomHeader_wrapper">
|
||||
<div className="mx_RoomHeader_simpleHeader">
|
||||
{ icon }
|
||||
{ this.props.title }
|
||||
|
|
|
@ -403,8 +403,7 @@ export default class Stickerpicker extends React.PureComponent {
|
|||
onClick={this._onHideStickersClick}
|
||||
active={this.state.showStickers.toString()}
|
||||
title={_t("Hide Stickers")}
|
||||
>
|
||||
</AccessibleButton>;
|
||||
/>;
|
||||
|
||||
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
|
||||
stickerPicker = <ContextMenu
|
||||
|
@ -431,8 +430,7 @@ export default class Stickerpicker extends React.PureComponent {
|
|||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||
onClick={this._onShowStickersClick}
|
||||
title={_t("Show Stickers")}
|
||||
>
|
||||
</AccessibleTooltipButton>;
|
||||
/>;
|
||||
}
|
||||
return <React.Fragment>
|
||||
{ stickersButton }
|
||||
|
|
|
@ -32,14 +32,16 @@ export default class TopUnreadMessagesBar extends React.Component {
|
|||
render() {
|
||||
return (
|
||||
<div className="mx_TopUnreadMessagesBar">
|
||||
<AccessibleButton className="mx_TopUnreadMessagesBar_scrollUp"
|
||||
<AccessibleButton
|
||||
className="mx_TopUnreadMessagesBar_scrollUp"
|
||||
title={_t('Jump to first unread message.')}
|
||||
onClick={this.props.onScrollUpClick}>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_TopUnreadMessagesBar_markAsRead"
|
||||
onClick={this.props.onScrollUpClick}
|
||||
/>
|
||||
<AccessibleButton
|
||||
className="mx_TopUnreadMessagesBar_markAsRead"
|
||||
title={_t('Mark all as read')}
|
||||
onClick={this.props.onCloseClick}>
|
||||
</AccessibleButton>
|
||||
onClick={this.props.onCloseClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import React, { ReactNode } from "react";
|
|||
import {
|
||||
RecordingState,
|
||||
VoiceRecording,
|
||||
} from "../../../voice/VoiceRecording";
|
||||
} from "../../../audio/VoiceRecording";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
|
@ -68,37 +68,49 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
}
|
||||
|
||||
await this.state.recorder.stop();
|
||||
const upload = await this.state.recorder.upload(this.props.room.roomId);
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
"body": "Voice message",
|
||||
//"msgtype": "org.matrix.msc2516.voice",
|
||||
"msgtype": MsgType.Audio,
|
||||
"url": upload.mxc,
|
||||
"file": upload.encrypted,
|
||||
"info": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
mimetype: this.state.recorder.contentType,
|
||||
size: this.state.recorder.contentLength,
|
||||
},
|
||||
|
||||
// MSC1767 + Ideals of MSC2516 as MSC3245
|
||||
// https://github.com/matrix-org/matrix-doc/pull/3245
|
||||
"org.matrix.msc1767.text": "Voice message",
|
||||
"org.matrix.msc1767.file": {
|
||||
url: upload.mxc,
|
||||
file: upload.encrypted,
|
||||
name: "Voice message.ogg",
|
||||
mimetype: this.state.recorder.contentType,
|
||||
size: this.state.recorder.contentLength,
|
||||
},
|
||||
"org.matrix.msc1767.audio": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
try {
|
||||
const upload = await this.state.recorder.upload(this.props.room.roomId);
|
||||
|
||||
// https://github.com/matrix-org/matrix-doc/pull/3246
|
||||
waveform: this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
|
||||
},
|
||||
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||
});
|
||||
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
"body": "Voice message",
|
||||
//"msgtype": "org.matrix.msc2516.voice",
|
||||
"msgtype": MsgType.Audio,
|
||||
"url": upload.mxc,
|
||||
"file": upload.encrypted,
|
||||
"info": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
mimetype: this.state.recorder.contentType,
|
||||
size: this.state.recorder.contentLength,
|
||||
},
|
||||
|
||||
// MSC1767 + Ideals of MSC2516 as MSC3245
|
||||
// https://github.com/matrix-org/matrix-doc/pull/3245
|
||||
"org.matrix.msc1767.text": "Voice message",
|
||||
"org.matrix.msc1767.file": {
|
||||
url: upload.mxc,
|
||||
file: upload.encrypted,
|
||||
name: "Voice message.ogg",
|
||||
mimetype: this.state.recorder.contentType,
|
||||
size: this.state.recorder.contentLength,
|
||||
},
|
||||
"org.matrix.msc1767.audio": {
|
||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
|
||||
// https://github.com/matrix-org/matrix-doc/pull/3246
|
||||
waveform: this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
|
||||
},
|
||||
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending/uploading voice message:", e);
|
||||
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
||||
title: _t('Upload Failed'),
|
||||
description: _t("The voice message failed to upload."),
|
||||
});
|
||||
return; // don't dispose the recording so the user can retry, maybe
|
||||
}
|
||||
await this.disposeRecording();
|
||||
}
|
||||
|
||||
|
@ -177,7 +189,6 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
|
||||
|
||||
if (this.state.recordingPhase !== RecordingState.Started) {
|
||||
// TODO: @@ TR: Should we disable this during upload? What does a failed upload look like?
|
||||
return <RecordingPlayback playback={this.state.recorder.getPlayback()} />;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue