Support for sending voice messages as replies and in threads (#9097)

* Support for sending voice messages as replies and in threads

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add tests

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2022-07-26 10:38:05 +02:00 committed by GitHub
parent b4b146f551
commit 60696d78ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 124 additions and 20 deletions

View file

@ -71,7 +71,6 @@ describe("Timeline", () => {
let oldAvatarUrl: string; let oldAvatarUrl: string;
let newAvatarUrl: string; let newAvatarUrl: string;
describe("useOnlyCurrentProfiles", () => {
beforeEach(() => { beforeEach(() => {
cy.startSynapse("default").then(data => { cy.startSynapse("default").then(data => {
synapse = data; synapse = data;
@ -81,18 +80,20 @@ describe("Timeline", () => {
roomId = _room1Id; roomId = _room1Id;
}); });
}), }),
).then(() => { );
});
});
describe("useOnlyCurrentProfiles", () => {
beforeEach(() => {
cy.uploadContent(OLD_AVATAR).then((url) => { cy.uploadContent(OLD_AVATAR).then((url) => {
oldAvatarUrl = url; oldAvatarUrl = url;
cy.setAvatarUrl(url); cy.setAvatarUrl(url);
}); });
}).then(() => {
cy.uploadContent(NEW_AVATAR).then((url) => { cy.uploadContent(NEW_AVATAR).then((url) => {
newAvatarUrl = url; newAvatarUrl = url;
}); });
}); });
});
});
afterEach(() => { afterEach(() => {
cy.stopSynapse(synapse); cy.stopSynapse(synapse);
@ -142,7 +143,9 @@ describe("Timeline", () => {
expectAvatar(e, newAvatarUrl); expectAvatar(e, newAvatarUrl);
}); });
}); });
});
describe("message displaying", () => {
it("should create and configure a room on IRC layout", () => { it("should create and configure a room on IRC layout", () => {
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
@ -216,4 +219,45 @@ describe("Timeline", () => {
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]"); cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]");
}); });
}); });
describe("message sending", () => {
const MESSAGE = "Hello world";
const viewRoomSendMessageAndSetupReply = () => {
// View room
cy.visit("/#/room/" + roomId);
// Send a message
cy.getComposer().type(`${MESSAGE}{enter}`);
// Reply to the message
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile_line", "Hello world").within(() => {
cy.get('[aria-label="Reply"]').click({ force: true }); // Cypress has no ability to hover
});
};
it("can reply with a text message", () => {
const reply = "Reply";
viewRoomSendMessageAndSetupReply();
cy.getComposer().type(`${reply}{enter}`);
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line").find(".mx_ReplyTile .mx_MTextBody")
.should("contain", MESSAGE);
cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody").contains(reply)
.should("have.length", 1);
});
it("can reply with a voice message", () => {
viewRoomSendMessageAndSetupReply();
cy.openMessageComposerOptions().find(`[aria-label="Voice Message"]`).click();
cy.wait(3000);
cy.getComposer().find(".mx_MessageComposer_sendMessage").click();
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line").find(".mx_ReplyTile .mx_MTextBody")
.should("contain", MESSAGE);
cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MVoiceMessageBody")
.should("have.length", 1);
});
});
}); });

View file

@ -213,6 +213,34 @@ describe("Threads", () => {
.should("contain", "I'm very good thanks :)"); .should("contain", "I'm very good thanks :)");
}); });
it("can send voice messages", () => {
// Increase viewport size and right-panel size, so that voice messages fit
cy.viewport(1280, 720);
cy.window().then((window) => {
window.localStorage.setItem("mx_rhs_size", "600");
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
roomId = _roomId;
cy.visit("/#/room/" + roomId);
});
// Send message
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
// Create thread
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
cy.openMessageComposerOptions(true).find(`[aria-label="Voice Message"]`).click();
cy.wait(3000);
cy.getComposer(true).find(".mx_MessageComposer_sendMessage").click();
cy.get(".mx_ThreadView .mx_MVoiceMessageBody").should("have.length", 1);
});
it("right panel behaves correctly", () => { it("right panel behaves correctly", () => {
// Create room // Create room
let roomId: string; let roomId: string;

View file

@ -383,7 +383,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
controls.push(<VoiceRecordComposerTile controls.push(<VoiceRecordComposerTile
key="controls_voice_record" key="controls_voice_record"
ref={this.voiceRecordingButton} ref={this.voiceRecordingButton}
room={this.props.room} />); room={this.props.room}
permalinkCreator={this.props.permalinkCreator}
relation={this.props.relation}
replyToEvent={this.props.replyToEvent} />);
} else if (this.context.tombstone) { } else if (this.context.tombstone) {
const replacementRoomId = this.context.tombstone.getContent()['replacement_room']; const replacementRoomId = this.context.tombstone.getContent()['replacement_room'];

View file

@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { MsgType } from "matrix-js-sdk/src/@types/event"; import { MsgType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
@ -38,9 +39,17 @@ import { NotificationColor } from "../../../stores/notifications/NotificationCol
import InlineSpinner from "../elements/InlineSpinner"; import InlineSpinner from "../elements/InlineSpinner";
import { PlaybackManager } from "../../../audio/PlaybackManager"; import { PlaybackManager } from "../../../audio/PlaybackManager";
import { doMaybeLocalRoomAction } from "../../../utils/local-room"; import { doMaybeLocalRoomAction } from "../../../utils/local-room";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { attachRelation } from "./SendMessageComposer";
import { addReplyToMessageContent } from "../../../utils/Reply";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import RoomContext from "../../../contexts/RoomContext";
interface IProps { interface IProps {
room: Room; room: Room;
permalinkCreator?: RoomPermalinkCreator;
relation?: IEventRelation;
replyToEvent?: MatrixEvent;
} }
interface IState { interface IState {
@ -53,7 +62,10 @@ interface IState {
* Container tile for rendering the voice message recorder in the composer. * Container tile for rendering the voice message recorder in the composer.
*/ */
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> { export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
public constructor(props) { static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
public constructor(props: IProps) {
super(props); super(props);
this.state = { this.state = {
@ -88,6 +100,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
throw new Error("No recording started - cannot send anything"); throw new Error("No recording started - cannot send anything");
} }
const { replyToEvent, relation, permalinkCreator } = this.props;
await this.state.recorder.stop(); await this.state.recorder.stop();
let upload: IUpload; let upload: IUpload;
@ -135,6 +149,21 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint "org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
}; };
attachRelation(content, relation);
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, {
permalinkCreator,
includeLegacyFallback: true,
});
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
defaultDispatcher.dispatch({
action: 'reply_to_event',
event: null,
context: this.context.timelineRenderingType,
});
}
doMaybeLocalRoomAction( doMaybeLocalRoomAction(
this.props.room.roomId, this.props.room.roomId,
(actualRoomId: string) => MatrixClientPeg.get().sendMessage(actualRoomId, content), (actualRoomId: string) => MatrixClientPeg.get().sendMessage(actualRoomId, content),