Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit a0c35d0

Browse files
authored
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
1 parent 4a23630 commit a0c35d0

File tree

13 files changed

+469
-45
lines changed

13 files changed

+469
-45
lines changed

res/css/views/rooms/_MessageComposer.pcss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ limitations under the License.
245245
mask-image: url('$(res)/img/voip/mic-on-mask.svg');
246246
}
247247

248+
.mx_MessageComposer_voiceBroadcast::before {
249+
mask-image: url('$(res)/img/element-icons/live.svg');
250+
}
251+
248252
.mx_MessageComposer_emoji::before {
249253
mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg');
250254
}

res/img/element-icons/live.svg

Lines changed: 58 additions & 0 deletions
Loading

src/Modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export class ModalManager {
103103
}
104104

105105
public createDialog<T extends any[]>(
106-
Element: React.ComponentType,
106+
Element: React.ComponentType<any>,
107107
...rest: ParametersWithoutFirst<ModalManager["createDialogAsync"]>
108108
) {
109109
return this.createDialogAsync<T>(Promise.resolve(Element), ...rest);

src/components/views/location/LocationButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import LocationShareMenu from './LocationShareMenu';
2828
interface IProps {
2929
roomId: string;
3030
sender: RoomMember;
31-
menuPosition: AboveLeftOf;
31+
menuPosition?: AboveLeftOf;
3232
relation?: IEventRelation;
3333
}
3434

src/components/views/rooms/MessageComposer.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import MessageComposerButtons from './MessageComposerButtons';
5252
import { ButtonEvent } from '../elements/AccessibleButton';
5353
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
5454
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
55+
import { Features } from '../../../settings/Settings';
5556

5657
let instanceCount = 0;
5758

@@ -89,10 +90,11 @@ interface IState {
8990
isStickerPickerOpen: boolean;
9091
showStickersButton: boolean;
9192
showPollsButton: boolean;
93+
showVoiceBroadcastButton: boolean;
9294
}
9395

9496
export default class MessageComposer extends React.Component<IProps, IState> {
95-
private dispatcherRef: string;
97+
private dispatcherRef?: string;
9698
private messageComposerInput = createRef<SendMessageComposerClass>();
9799
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
98100
private ref: React.RefObject<HTMLDivElement> = createRef();
@@ -114,17 +116,19 @@ export default class MessageComposer extends React.Component<IProps, IState> {
114116
this.state = {
115117
isComposerEmpty: true,
116118
haveRecording: false,
117-
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
119+
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
118120
isMenuOpen: false,
119121
isStickerPickerOpen: false,
120122
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
121123
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
124+
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
122125
};
123126

124127
this.instanceId = instanceCount++;
125128

126129
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
127130
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
131+
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
128132
}
129133

130134
private get voiceRecording(): Optional<VoiceRecording> {
@@ -153,7 +157,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
153157
public componentDidMount() {
154158
this.dispatcherRef = dis.register(this.onAction);
155159
this.waitForOwnMember();
156-
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current);
160+
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!);
157161
UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize);
158162
this.updateRecordingState(); // grab any cached recordings
159163
}
@@ -199,13 +203,19 @@ export default class MessageComposer extends React.Component<IProps, IState> {
199203
}
200204
break;
201205
}
206+
case Features.VoiceBroadcast: {
207+
if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) {
208+
this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue });
209+
}
210+
break;
211+
}
202212
}
203213
}
204214
}
205215
};
206216

207217
private waitForOwnMember() {
208-
// if we have the member already, do that
218+
// If we have the member already, do that
209219
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
210220
if (me) {
211221
this.setState({ me });
@@ -242,6 +252,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
242252
}
243253

244254
const viaServers = [this.context.tombstone.getSender().split(':').slice(1).join(':')];
255+
245256
dis.dispatch<ViewRoomPayload>({
246257
action: Action.ViewRoom,
247258
highlighted: true,
@@ -426,8 +437,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
426437
}
427438

428439
let recordingTooltip;
429-
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
430-
if (secondsLeft) {
440+
if (this.state.recordingTimeLeftSeconds) {
441+
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
431442
recordingTooltip = <Tooltip
432443
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
433444
alignment={Alignment.Top}
@@ -484,6 +495,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
484495
showPollsButton={this.state.showPollsButton}
485496
showStickersButton={this.showStickersButton}
486497
toggleButtonMenu={this.toggleButtonMenu}
498+
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
499+
onStartVoiceBroadcastClick={() => {
500+
// Sends a voice message. To be replaced by voice broadcast during development.
501+
this.voiceRecordingButton.current?.onRecordStartEndClick();
502+
if (this.context.narrow) {
503+
this.toggleButtonMenu();
504+
}
505+
}}
487506
/> }
488507
{ showSendButton && (
489508
<SendButton

src/components/views/rooms/MessageComposerButtons.tsx

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,16 @@ interface IProps {
4545
haveRecording: boolean;
4646
isMenuOpen: boolean;
4747
isStickerPickerOpen: boolean;
48-
menuPosition: AboveLeftOf;
48+
menuPosition?: AboveLeftOf;
4949
onRecordStartEndClick: () => void;
5050
relation?: IEventRelation;
5151
setStickerPickerOpen: (isStickerPickerOpen: boolean) => void;
5252
showLocationButton: boolean;
5353
showPollsButton: boolean;
5454
showStickersButton: boolean;
5555
toggleButtonMenu: () => void;
56+
showVoiceBroadcastButton: boolean;
57+
onStartVoiceBroadcastClick: () => void;
5658
}
5759

5860
type OverflowMenuCloser = () => void;
@@ -76,7 +78,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
7678
uploadButton(), // props passed via UploadButtonContext
7779
showStickersButton(props),
7880
voiceRecordingButton(props, narrow),
79-
props.showPollsButton && pollButton(room, props.relation),
81+
startVoiceBroadcastButton(props),
82+
props.showPollsButton ? pollButton(room, props.relation) : null,
8083
showLocationButton(props, room, roomId, matrixClient),
8184
];
8285
} else {
@@ -87,7 +90,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
8790
moreButtons = [
8891
showStickersButton(props),
8992
voiceRecordingButton(props, narrow),
90-
props.showPollsButton && pollButton(room, props.relation),
93+
startVoiceBroadcastButton(props),
94+
props.showPollsButton ? pollButton(room, props.relation) : null,
9195
showLocationButton(props, room, roomId, matrixClient),
9296
];
9397
}
@@ -265,7 +269,7 @@ const UploadButton = () => {
265269
/>;
266270
};
267271

268-
function showStickersButton(props: IProps): ReactElement {
272+
function showStickersButton(props: IProps): ReactElement | null {
269273
return (
270274
props.showStickersButton
271275
? <CollapsibleButton
@@ -280,7 +284,21 @@ function showStickersButton(props: IProps): ReactElement {
280284
);
281285
}
282286

283-
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement {
287+
const startVoiceBroadcastButton: React.FC<IProps> = (props: IProps): ReactElement | null => {
288+
return (
289+
props.showVoiceBroadcastButton
290+
? <CollapsibleButton
291+
key="start_voice_broadcast"
292+
className="mx_MessageComposer_button"
293+
iconClassName="mx_MessageComposer_voiceBroadcast"
294+
onClick={props.onStartVoiceBroadcastClick}
295+
title={_t("Voice broadcast")}
296+
/>
297+
: null
298+
);
299+
};
300+
301+
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null {
284302
// XXX: recording UI does not work well in narrow mode, so hide for now
285303
return (
286304
narrow
@@ -312,7 +330,7 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
312330
this.context?.(); // close overflow menu
313331
const canSend = this.props.room.currentState.maySendEvent(
314332
M_POLL_START.name,
315-
MatrixClientPeg.get().getUserId(),
333+
MatrixClientPeg.get().getUserId()!,
316334
);
317335
if (!canSend) {
318336
Modal.createDialog(
@@ -362,14 +380,16 @@ function showLocationButton(
362380
room: Room,
363381
roomId: string,
364382
matrixClient: MatrixClient,
365-
): ReactElement {
383+
): ReactElement | null {
384+
const sender = room.getMember(matrixClient.getUserId()!);
385+
366386
return (
367-
props.showLocationButton
387+
props.showLocationButton && sender
368388
? <LocationButton
369389
key="location"
370390
roomId={roomId}
371391
relation={props.relation}
372-
sender={room.getMember(matrixClient.getUserId())}
392+
sender={sender}
373393
menuPosition={props.menuPosition}
374394
/>
375395
: null

src/components/views/rooms/ReplyPreview.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ function cancelQuoting(context: TimelineRenderingType) {
3434

3535
interface IProps {
3636
permalinkCreator: RoomPermalinkCreator;
37-
replyToEvent: MatrixEvent;
37+
replyToEvent?: MatrixEvent;
3838
}
3939

4040
export default class ReplyPreview extends React.Component<IProps> {
4141
public static contextType = RoomContext;
4242

43-
public render(): JSX.Element {
43+
public render(): JSX.Element | null {
4444
if (!this.props.replyToEvent) return null;
4545

4646
return <div className="mx_ReplyPreview">

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,7 @@
911911
"Sliding Sync mode (under active development, cannot be disabled)": "Sliding Sync mode (under active development, cannot be disabled)",
912912
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
913913
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
914+
"Voice broadcast (under active development)": "Voice broadcast (under active development)",
914915
"Use new session manager (under active development)": "Use new session manager (under active development)",
915916
"Font size": "Font size",
916917
"Use custom size": "Use custom size",
@@ -1813,6 +1814,7 @@
18131814
"Emoji": "Emoji",
18141815
"Hide stickers": "Hide stickers",
18151816
"Sticker": "Sticker",
1817+
"Voice broadcast": "Voice broadcast",
18161818
"Voice Message": "Voice Message",
18171819
"You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.",
18181820
"Poll": "Poll",

src/settings/Settings.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export enum LabGroup {
101101
Developer,
102102
}
103103

104+
export enum Features {
105+
VoiceBroadcast = "feature_voice_broadcast",
106+
}
107+
104108
export const labGroupNames: Record<LabGroup, string> = {
105109
[LabGroup.Messaging]: _td("Messaging"),
106110
[LabGroup.Profile]: _td("Profile"),
@@ -435,6 +439,13 @@ export const SETTINGS: {[setting: string]: ISetting} = {
435439
displayName: _td("Favourite Messages (under active development)"),
436440
default: false,
437441
},
442+
[Features.VoiceBroadcast]: {
443+
isFeature: true,
444+
labsGroup: LabGroup.Messaging,
445+
supportedLevels: LEVELS_FEATURE,
446+
displayName: _td("Voice broadcast (under active development)"),
447+
default: false,
448+
},
438449
"feature_new_device_manager": {
439450
isFeature: true,
440451
labsGroup: LabGroup.Experimental,

0 commit comments

Comments
 (0)