Add Voice Broadcast labs setting and composer button (#9279)

* Add Voice Broadcast labs setting and composer button

* Implement strict typing

* Extend MessageComposer-test

* Extend tests

* Revert some strict type fixex

* Convert FEATURES to enum; change case

* Use fake timers in MessageComposer-test
This commit is contained in:
Michael Weimann 2022-09-16 11:10:33 +02:00 committed by GitHub
parent 4a23630e06
commit a0c35d088a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 469 additions and 45 deletions

View file

@ -52,6 +52,7 @@ import MessageComposerButtons from './MessageComposerButtons';
import { ButtonEvent } from '../elements/AccessibleButton';
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
import { Features } from '../../../settings/Settings';
let instanceCount = 0;
@ -89,10 +90,11 @@ interface IState {
isStickerPickerOpen: boolean;
showStickersButton: boolean;
showPollsButton: boolean;
showVoiceBroadcastButton: boolean;
}
export default class MessageComposer extends React.Component<IProps, IState> {
private dispatcherRef: string;
private dispatcherRef?: string;
private messageComposerInput = createRef<SendMessageComposerClass>();
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
private ref: React.RefObject<HTMLDivElement> = createRef();
@ -114,17 +116,19 @@ export default class MessageComposer extends React.Component<IProps, IState> {
this.state = {
isComposerEmpty: true,
haveRecording: false,
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
isMenuOpen: false,
isStickerPickerOpen: false,
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
};
this.instanceId = instanceCount++;
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
}
private get voiceRecording(): Optional<VoiceRecording> {
@ -153,7 +157,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
public componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.waitForOwnMember();
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current);
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!);
UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize);
this.updateRecordingState(); // grab any cached recordings
}
@ -199,13 +203,19 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
break;
}
case Features.VoiceBroadcast: {
if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) {
this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue });
}
break;
}
}
}
}
};
private waitForOwnMember() {
// if we have the member already, do that
// If we have the member already, do that
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
if (me) {
this.setState({ me });
@ -242,6 +252,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
const viaServers = [this.context.tombstone.getSender().split(':').slice(1).join(':')];
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
highlighted: true,
@ -426,8 +437,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
let recordingTooltip;
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
if (secondsLeft) {
if (this.state.recordingTimeLeftSeconds) {
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
recordingTooltip = <Tooltip
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
alignment={Alignment.Top}
@ -484,6 +495,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
showPollsButton={this.state.showPollsButton}
showStickersButton={this.showStickersButton}
toggleButtonMenu={this.toggleButtonMenu}
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
onStartVoiceBroadcastClick={() => {
// Sends a voice message. To be replaced by voice broadcast during development.
this.voiceRecordingButton.current?.onRecordStartEndClick();
if (this.context.narrow) {
this.toggleButtonMenu();
}
}}
/> }
{ showSendButton && (
<SendButton

View file

@ -45,7 +45,7 @@ interface IProps {
haveRecording: boolean;
isMenuOpen: boolean;
isStickerPickerOpen: boolean;
menuPosition: AboveLeftOf;
menuPosition?: AboveLeftOf;
onRecordStartEndClick: () => void;
relation?: IEventRelation;
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
@ -53,6 +53,8 @@ interface IProps {
showPollsButton: boolean;
showStickersButton: boolean;
toggleButtonMenu: () => void;
showVoiceBroadcastButton: boolean;
onStartVoiceBroadcastClick: () => void;
}
type OverflowMenuCloser = () => void;
@ -76,7 +78,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
uploadButton(), // props passed via UploadButtonContext
showStickersButton(props),
voiceRecordingButton(props, narrow),
props.showPollsButton && pollButton(room, props.relation),
startVoiceBroadcastButton(props),
props.showPollsButton ? pollButton(room, props.relation) : null,
showLocationButton(props, room, roomId, matrixClient),
];
} else {
@ -87,7 +90,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
moreButtons = [
showStickersButton(props),
voiceRecordingButton(props, narrow),
props.showPollsButton && pollButton(room, props.relation),
startVoiceBroadcastButton(props),
props.showPollsButton ? pollButton(room, props.relation) : null,
showLocationButton(props, room, roomId, matrixClient),
];
}
@ -265,7 +269,7 @@ const UploadButton = () => {
/>;
};
function showStickersButton(props: IProps): ReactElement {
function showStickersButton(props: IProps): ReactElement | null {
return (
props.showStickersButton
? <CollapsibleButton
@ -280,7 +284,21 @@ function showStickersButton(props: IProps): ReactElement {
);
}
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement {
const startVoiceBroadcastButton: React.FC<IProps> = (props: IProps): ReactElement | null => {
return (
props.showVoiceBroadcastButton
? <CollapsibleButton
key="start_voice_broadcast"
className="mx_MessageComposer_button"
iconClassName="mx_MessageComposer_voiceBroadcast"
onClick={props.onStartVoiceBroadcastClick}
title={_t("Voice broadcast")}
/>
: null
);
};
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null {
// XXX: recording UI does not work well in narrow mode, so hide for now
return (
narrow
@ -312,7 +330,7 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
this.context?.(); // close overflow menu
const canSend = this.props.room.currentState.maySendEvent(
M_POLL_START.name,
MatrixClientPeg.get().getUserId(),
MatrixClientPeg.get().getUserId()!,
);
if (!canSend) {
Modal.createDialog(
@ -362,14 +380,16 @@ function showLocationButton(
room: Room,
roomId: string,
matrixClient: MatrixClient,
): ReactElement {
): ReactElement | null {
const sender = room.getMember(matrixClient.getUserId()!);
return (
props.showLocationButton
props.showLocationButton && sender
? <LocationButton
key="location"
roomId={roomId}
relation={props.relation}
sender={room.getMember(matrixClient.getUserId())}
sender={sender}
menuPosition={props.menuPosition}
/>
: null

View file

@ -34,13 +34,13 @@ function cancelQuoting(context: TimelineRenderingType) {
interface IProps {
permalinkCreator: RoomPermalinkCreator;
replyToEvent: MatrixEvent;
replyToEvent?: MatrixEvent;
}
export default class ReplyPreview extends React.Component<IProps> {
public static contextType = RoomContext;
public render(): JSX.Element {
public render(): JSX.Element | null {
if (!this.props.replyToEvent) return null;
return <div className="mx_ReplyPreview">