Merge branch 'develop' into weeman1337/mute-broadcast-notifications

This commit is contained in:
Michael Weimann 2023-01-05 09:23:24 +01:00
commit 3a9268e963
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
57 changed files with 984 additions and 694 deletions

View file

@ -41,7 +41,6 @@ import { DefaultTagID } from "../../stores/room-list/models";
import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel from "./LeftPanel";
import PipContainer from "../views/voip/PipContainer";
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer";
@ -71,6 +70,7 @@ import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload
import { IConfigOptions } from "../../IConfigOptions";
import LeftPanelLiveShareWarning from "../views/beacon/LeftPanelLiveShareWarning";
import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage";
import { PipContainer } from "./PipContainer";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.

View file

@ -16,9 +16,9 @@ limitations under the License.
import React, { createRef } from "react";
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
import { lerp } from "../../../utils/AnimationUtils";
import { MarkedExecution } from "../../../utils/MarkedExecution";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
import { lerp } from "../../utils/AnimationUtils";
import { MarkedExecution } from "../../utils/MarkedExecution";
const PIP_VIEW_WIDTH = 336;
const PIP_VIEW_HEIGHT = 232;
@ -65,12 +65,20 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_HEIGHT;
private translationX = this.desiredTranslationX;
private translationY = this.desiredTranslationY;
private moving = false;
private scheduledUpdate = new MarkedExecution(
private mouseHeld = false;
private scheduledUpdate: MarkedExecution = new MarkedExecution(
() => this.animationCallback(),
() => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
);
private _moving = false;
public get moving(): boolean {
return this._moving;
}
private set moving(value: boolean) {
this._moving = value;
}
public componentDidMount() {
document.addEventListener("mousemove", this.onMoving);
document.addEventListener("mouseup", this.onEndMoving);
@ -183,26 +191,47 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
event.preventDefault();
event.stopPropagation();
this.moving = true;
this.initX = event.pageX - this.desiredTranslationX;
this.initY = event.pageY - this.desiredTranslationY;
this.scheduledUpdate.mark();
this.mouseHeld = true;
};
private onMoving = (event: React.MouseEvent | MouseEvent) => {
if (!this.moving) return;
private onMoving = (event: MouseEvent) => {
if (!this.mouseHeld) return;
event.preventDefault();
event.stopPropagation();
if (!this.moving) {
this.moving = true;
this.initX = event.pageX - this.desiredTranslationX;
this.initY = event.pageY - this.desiredTranslationY;
this.scheduledUpdate.mark();
}
this.setTranslation(event.pageX - this.initX, event.pageY - this.initY);
};
private onEndMoving = () => {
this.moving = false;
private onEndMoving = (event: MouseEvent) => {
if (!this.mouseHeld) return;
event.preventDefault();
event.stopPropagation();
this.mouseHeld = false;
// Delaying this to the next event loop tick is necessary for click
// event cancellation to work
setImmediate(() => (this.moving = false));
this.snap(true);
};
private onClickCapture = (event: React.MouseEvent) => {
// To prevent mouse up events during dragging from being double-counted
// as clicks, we cancel clicks before they ever reach the target
if (this.moving) {
event.preventDefault();
event.stopPropagation();
}
};
public render() {
const style = {
transform: `translateX(${this.translationX}px) translateY(${this.translationY}px)`,
@ -220,6 +249,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
className={this.props.className}
style={style}
ref={this.callViewWrapper}
onClickCapture={this.onClickCapture}
onDoubleClick={this.props.onDoubleClick}
>
{children}

View file

@ -14,28 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef, useContext } from "react";
import React, { MutableRefObject, useContext, useRef } from "react";
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { Optional } from "matrix-events-sdk";
import LegacyCallView from "./LegacyCallView";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler";
import PersistentApp from "../elements/PersistentApp";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import LegacyCallView from "../views/voip/LegacyCallView";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import PictureInPictureDragger, { CreatePipChildren } from "./PictureInPictureDragger";
import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import LegacyCallViewHeader from "./LegacyCallView/LegacyCallViewHeader";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../../stores/ActiveWidgetStore";
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { SDKContext, SdkContextClass } from "../../../contexts/SDKContext";
import { CallStore } from "../../../stores/CallStore";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
import {
useCurrentVoiceBroadcastPreRecording,
useCurrentVoiceBroadcastRecording,
@ -46,8 +40,9 @@ import {
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
VoiceBroadcastSmallPlaybackBody,
} from "../../../voice-broadcast";
import { useCurrentVoiceBroadcastPlayback } from "../../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
} from "../../voice-broadcast";
import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
import { WidgetPip } from "../views/pips/WidgetPip";
const SHOW_CALL_IN_STATES = [
CallState.Connected,
@ -59,9 +54,10 @@ const SHOW_CALL_IN_STATES = [
];
interface IProps {
voiceBroadcastRecording?: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording?: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback?: Optional<VoiceBroadcastPlayback>;
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
movePersistedElement: MutableRefObject<(() => void) | undefined>;
}
interface IState {
@ -78,20 +74,8 @@ interface IState {
persistentWidgetId: string;
persistentRoomId: string;
showWidgetInPip: boolean;
moving: boolean;
}
const getRoomAndAppForWidget = (widgetId: string, roomId: string): [Room | null, IApp | null] => {
if (!widgetId) return [null, null];
if (!roomId) return [null, null];
const room = MatrixClientPeg.get().getRoom(roomId);
const app = WidgetStore.instance.getApps(roomId).find((app) => app.id === widgetId);
return [room, app || null];
};
// Splits a list of calls into one 'primary' one and a list
// (which should be a single element) of other calls.
// The primary will be the one not on hold, or an arbitrary one
@ -128,16 +112,12 @@ function getPrimarySecondaryCallsForPip(roomId: Optional<string>): [MatrixCall |
}
/**
* PipView shows a small version of the LegacyCallView or a sticky widget hovering over the UI in 'picture-in-picture'
* (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
* PipContainer shows a small version of the LegacyCallView or a sticky widget hovering over the UI in
* 'picture-in-picture' (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
* and all widgets that are active but not shown in any other possible container.
*/
class PipView extends React.Component<IProps, IState> {
// The cast is not so great, but solves the typing issue for the moment.
// Proper solution: use useRef (requires the component to be refactored to a functional component).
private movePersistedElement = createRef<() => void>() as React.MutableRefObject<() => void>;
class PipContainerInner extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
@ -146,7 +126,6 @@ class PipView extends React.Component<IProps, IState> {
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
this.state = {
moving: false,
viewedRoomId: roomId || undefined,
primaryCall: primaryCall || null,
secondaryCall: secondaryCalls[0],
@ -168,7 +147,6 @@ class PipView extends React.Component<IProps, IState> {
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
document.addEventListener("mouseup", this.onEndMoving.bind(this));
}
public componentWillUnmount() {
@ -184,18 +162,9 @@ class PipView extends React.Component<IProps, IState> {
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
document.removeEventListener("mouseup", this.onEndMoving.bind(this));
}
private onStartMoving() {
this.setState({ moving: true });
}
private onEndMoving() {
this.setState({ moving: false });
}
private onMove = () => this.movePersistedElement.current?.();
private onMove = () => this.props.movePersistedElement.current?.();
private onRoomViewStoreUpdate = () => {
const newRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
@ -265,53 +234,6 @@ class PipView extends React.Component<IProps, IState> {
}
};
private onMaximize = (): void => {
const widgetId = this.state.persistentWidgetId;
const roomId = this.state.persistentRoomId;
if (this.state.showWidgetInPip && widgetId && roomId) {
const [room, app] = getRoomAndAppForWidget(widgetId, roomId);
if (room && app) {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Center);
return;
}
}
dis.dispatch({
action: "video_fullscreen",
fullscreen: true,
});
};
private onPin = (): void => {
if (!this.state.showWidgetInPip) return;
const [room, app] = getRoomAndAppForWidget(this.state.persistentWidgetId, this.state.persistentRoomId);
if (room && app) {
WidgetLayoutStore.instance.moveToContainer(room, app, Container.Top);
}
};
private onExpand = (): void => {
const widgetId = this.state.persistentWidgetId;
if (!widgetId || !this.state.showWidgetInPip) return;
dis.dispatch({
action: Action.ViewRoom,
room_id: this.state.persistentRoomId,
});
};
private onViewCall = (): void =>
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.state.persistentRoomId,
view_call: true,
metricsTrigger: undefined,
});
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
public updateShowWidgetInPip(
persistentWidgetId = this.state.persistentWidgetId,
@ -398,36 +320,14 @@ class PipView extends React.Component<IProps, IState> {
}
if (this.state.showWidgetInPip) {
const pipViewClasses = classNames({
mx_LegacyCallView: true,
mx_LegacyCallView_pip: pipMode,
mx_LegacyCallView_large: !pipMode,
});
const roomId = this.state.persistentRoomId;
const roomForWidget = MatrixClientPeg.get().getRoom(roomId)!;
const viewingCallRoom = this.state.viewedRoomId === roomId;
const isCall = CallStore.instance.getActiveCall(roomId) !== null;
pipContent.push(({ onStartMoving }) => (
<div className={pipViewClasses}>
<LegacyCallViewHeader
onPipMouseDown={(event) => {
onStartMoving?.(event);
this.onStartMoving.bind(this)();
}}
pipMode={pipMode}
callRooms={[roomForWidget]}
onExpand={!isCall && !viewingCallRoom ? this.onExpand : undefined}
onPin={!isCall && viewingCallRoom ? this.onPin : undefined}
onMaximize={isCall ? this.onViewCall : viewingCallRoom ? this.onMaximize : undefined}
/>
<PersistentApp
persistentWidgetId={this.state.persistentWidgetId}
persistentRoomId={roomId}
pointerEvents={this.state.moving ? "none" : undefined}
movePersistedElement={this.movePersistedElement}
/>
</div>
<WidgetPip
widgetId={this.state.persistentWidgetId}
room={MatrixClientPeg.get().getRoom(this.state.persistentRoomId)!}
viewingRoom={this.state.viewedRoomId === this.state.persistentRoomId}
onStartMoving={onStartMoving}
movePersistedElement={this.props.movePersistedElement}
/>
));
}
@ -448,7 +348,7 @@ class PipView extends React.Component<IProps, IState> {
}
}
const PipViewHOC: React.FC<IProps> = (props) => {
export const PipContainer: React.FC = () => {
const sdkContext = useContext(SDKContext);
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
@ -459,14 +359,14 @@ const PipViewHOC: React.FC<IProps> = (props) => {
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
const movePersistedElement = useRef<() => void>();
return (
<PipView
<PipContainerInner
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
voiceBroadcastRecording={currentVoiceBroadcastRecording}
{...props}
movePersistedElement={movePersistedElement}
/>
);
};
export default PipViewHOC;

View file

@ -45,10 +45,12 @@ const BeaconMarker: React.FC<Props> = ({ map, beacon, tooltip }) => {
return null;
}
const geoUri = latestLocationState?.uri;
const geoUri = latestLocationState.uri || "";
const markerRoomMember =
beacon.beaconInfo.assetType === LocationAssetType.Self ? room.getMember(beacon.beaconInfoOwner) : undefined;
const assetTypeIsSelf = beacon.beaconInfo?.assetType === LocationAssetType.Self;
const _member = room?.getMember(beacon.beaconInfoOwner);
const markerRoomMember = assetTypeIsSelf && _member ? _member : undefined;
return (
<SmartMarker

View file

@ -85,7 +85,7 @@ interface IProps {
widgetPageTitle?: string;
showLayoutButtons?: boolean;
// Handle to manually notify the PersistedElement that it needs to move
movePersistedElement?: MutableRefObject<() => void>;
movePersistedElement?: MutableRefObject<(() => void) | undefined>;
}
interface IState {

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { MutableRefObject } from "react";
import ReactDOM from "react-dom";
import { throttle } from "lodash";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
import dis from "../../../dispatcher/dispatcher";
@ -58,7 +57,7 @@ interface IProps {
style?: React.StyleHTMLAttributes<HTMLDivElement>;
// Handle to manually notify this PersistedElement that it needs to move
moveRef?: MutableRefObject<() => void>;
moveRef?: MutableRefObject<(() => void) | undefined>;
}
/**
@ -177,24 +176,20 @@ export default class PersistedElement extends React.Component<IProps> {
child.style.display = visible ? "block" : "none";
}
private updateChildPosition = throttle(
(child: HTMLDivElement, parent: HTMLDivElement): void => {
if (!child || !parent) return;
private updateChildPosition(child: HTMLDivElement, parent: HTMLDivElement): void {
if (!child || !parent) return;
const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, {
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
position: "absolute",
top: "0",
left: "0",
transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`,
width: parentRect.width + "px",
height: parentRect.height + "px",
});
},
16,
{ trailing: true, leading: true },
);
const parentRect = parent.getBoundingClientRect();
Object.assign(child.style, {
zIndex: isNullOrUndefined(this.props.zIndex) ? 9 : this.props.zIndex,
position: "absolute",
top: "0",
left: "0",
transform: `translateX(${parentRect.left}px) translateY(${parentRect.top}px)`,
width: parentRect.width + "px",
height: parentRect.height + "px",
});
}
public render(): JSX.Element {
return <div ref={this.collectChildContainer} />;

View file

@ -27,7 +27,7 @@ interface IProps {
persistentWidgetId: string;
persistentRoomId: string;
pointerEvents?: string;
movePersistedElement: MutableRefObject<() => void>;
movePersistedElement: MutableRefObject<(() => void) | undefined>;
}
export default class PersistentApp extends React.Component<IProps> {

View file

@ -59,6 +59,7 @@ import { Action } from "../../../dispatcher/actions";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import useFavouriteMessages from "../../../hooks/useFavouriteMessages";
import { GetRelationsForEvent } from "../rooms/EventTile";
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
interface IOptionsButtonProps {
mxEvent: MatrixEvent;
@ -394,7 +395,8 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
* until cross-platform support
* (PSF-1041)
*/
!M_BEACON_INFO.matches(this.props.mxEvent.getType());
!M_BEACON_INFO.matches(this.props.mxEvent.getType()) &&
!(this.props.mxEvent.getType() === VoiceBroadcastInfoEventType);
return inNotThreadTimeline && isAllowedMessageType;
}

View file

@ -0,0 +1,140 @@
/*
Copyright 2022 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, { FC, MutableRefObject, useCallback, useMemo } from "react";
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
import PersistentApp from "../elements/PersistentApp";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../../dispatcher/actions";
import { useCallForWidget } from "../../../hooks/useCall";
import WidgetStore from "../../../stores/WidgetStore";
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import Toolbar from "../../../accessibility/Toolbar";
import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import { Icon as BackIcon } from "../../../../res/img/element-icons/back.svg";
import { Icon as HangupIcon } from "../../../../res/img/element-icons/call/hangup.svg";
import { _t } from "../../../languageHandler";
import { WidgetType } from "../../../widgets/WidgetType";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import { ElementWidgetActions } from "../../../stores/widgets/ElementWidgetActions";
import { Alignment } from "../elements/Tooltip";
interface Props {
widgetId: string;
room: Room;
viewingRoom: boolean;
onStartMoving: (e: React.MouseEvent<Element, MouseEvent>) => void;
movePersistedElement: MutableRefObject<(() => void) | undefined>;
}
/**
* A picture-in-picture view for a widget. Additional controls are shown if the
* widget is a call of some sort.
*/
export const WidgetPip: FC<Props> = ({ widgetId, room, viewingRoom, onStartMoving, movePersistedElement }) => {
const widget = useMemo(
() => WidgetStore.instance.getApps(room.roomId).find((app) => app.id === widgetId)!,
[room, widgetId],
);
const roomName = useTypedEventEmitterState(
room,
RoomEvent.Name,
useCallback(() => room.name, [room]),
);
const call = useCallForWidget(widgetId, room.roomId);
const onBackClick = useCallback(
(ev) => {
ev.preventDefault();
ev.stopPropagation();
if (call !== null) {
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
metricsTrigger: "WebFloatingCallWindow",
});
} else if (viewingRoom) {
WidgetLayoutStore.instance.moveToContainer(room, widget, Container.Center);
} else {
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: "WebFloatingCallWindow",
});
}
},
[room, call, widget, viewingRoom],
);
const onLeaveClick = useCallback(
(ev) => {
ev.preventDefault();
ev.stopPropagation();
if (call !== null) {
call.disconnect().catch((e) => console.error("Failed to leave call", e));
} else {
// Assumed to be a Jitsi widget
WidgetMessagingStore.instance
.getMessagingForUid(WidgetUtils.getWidgetUid(widget))
?.transport.send(ElementWidgetActions.HangupCall, {})
.catch((e) => console.error("Failed to leave Jitsi", e));
}
},
[call, widget],
);
return (
<div className="mx_WidgetPip" onMouseDown={onStartMoving} onClick={onBackClick}>
<Toolbar className="mx_WidgetPip_header">
<RovingAccessibleButton
onClick={onBackClick}
className="mx_WidgetPip_backButton"
aria-label={_t("Back")}
>
<BackIcon className="mx_Icon mx_Icon_16" />
{roomName}
</RovingAccessibleButton>
</Toolbar>
<PersistentApp
persistentWidgetId={widgetId}
persistentRoomId={room.roomId}
pointerEvents="none"
movePersistedElement={movePersistedElement}
/>
{(call !== null || WidgetType.JITSI.matches(widget.type)) && (
<Toolbar className="mx_WidgetPip_footer">
<RovingAccessibleTooltipButton
onClick={onLeaveClick}
tooltip={_t("Leave")}
aria-label={_t("Leave")}
alignment={Alignment.Top}
>
<HangupIcon className="mx_Icon mx_Icon_24" />
</RovingAccessibleTooltipButton>
</Toolbar>
)}
</div>
);
};

View file

@ -54,9 +54,8 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
import { Features } from "../../../settings/Settings";
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
import { SendWysiwygComposer, sendMessage } from "./wysiwyg_composer/";
import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysiwyg_composer/";
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
import { htmlToPlainText } from "../../../utils/room/htmlToPlaintext";
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
import { SdkContextClass } from "../../../contexts/SDKContext";
@ -333,7 +332,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
if (this.state.isWysiwygLabEnabled) {
const { permalinkCreator, relation, replyToEvent } = this.props;
sendMessage(this.state.composerContent, this.state.isRichTextEnabled, {
await sendMessage(this.state.composerContent, this.state.isRichTextEnabled, {
mxClient: this.props.mxClient,
roomContext: this.context,
permalinkCreator,
@ -358,14 +357,19 @@ export class MessageComposer extends React.Component<IProps, IState> {
});
};
private onRichTextToggle = () => {
this.setState((state) => ({
isRichTextEnabled: !state.isRichTextEnabled,
initialComposerContent: !state.isRichTextEnabled
? state.composerContent
: // TODO when available use rust model plain text
htmlToPlainText(state.composerContent),
}));
private onRichTextToggle = async () => {
const { richToPlain, plainToRich } = await getConversionFunctions();
const { isRichTextEnabled, composerContent } = this.state;
const convertedContent = isRichTextEnabled
? await richToPlain(composerContent)
: await plainToRich(composerContent);
this.setState({
isRichTextEnabled: !isRichTextEnabled,
composerContent: convertedContent,
initialComposerContent: convertedContent,
});
};
private onVoiceStoreUpdate = () => {

View file

@ -16,9 +16,25 @@ limitations under the License.
import React, { ComponentProps, lazy, Suspense } from "react";
// we need to import the types for TS, but do not import the sendMessage
// function to avoid importing from "@matrix-org/matrix-wysiwyg"
import { SendMessageParams } from "./utils/message";
const SendComposer = lazy(() => import("./SendWysiwygComposer"));
const EditComposer = lazy(() => import("./EditWysiwygComposer"));
export const dynamicImportSendMessage = async (message: string, isHTML: boolean, params: SendMessageParams) => {
const { sendMessage } = await import("./utils/message");
return sendMessage(message, isHTML, params);
};
export const dynamicImportConversionFunctions = async () => {
const { richToPlain, plainToRich } = await import("@matrix-org/matrix-wysiwyg");
return { richToPlain, plainToRich };
};
export function DynamicImportSendWysiwygComposer(props: ComponentProps<typeof SendComposer>) {
return (
<Suspense fallback={<div />}>

View file

@ -17,11 +17,22 @@ limitations under the License.
import { KeyboardEvent, SyntheticEvent, useCallback, useRef, useState } from "react";
import { useSettingValue } from "../../../../../hooks/useSettings";
import { IS_MAC, Key } from "../../../../../Keyboard";
function isDivElement(target: EventTarget): target is HTMLDivElement {
return target instanceof HTMLDivElement;
}
// Hitting enter inside the editor inserts an editable div, initially containing a <br />
// For correct display, first replace this pattern with a newline character and then remove divs
// noting that they are used to delimit paragraphs
function amendInnerHtml(text: string) {
return text
.replace(/<div><br><\/div>/g, "\n") // this is pressing enter then not typing
.replace(/<div>/g, "\n") // this is from pressing enter, then typing inside the div
.replace(/<\/div>/g, "");
}
export function usePlainTextListeners(
initialContent?: string,
onChange?: (content: string) => void,
@ -44,25 +55,39 @@ export function usePlainTextListeners(
[onChange],
);
const enterShouldSend = !useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
const onInput = useCallback(
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
if (isDivElement(event.target)) {
setText(event.target.innerHTML);
// if enterShouldSend, we do not need to amend the html before setting text
const newInnerHTML = enterShouldSend ? event.target.innerHTML : amendInnerHtml(event.target.innerHTML);
setText(newInnerHTML);
}
},
[setText],
[setText, enterShouldSend],
);
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
const onKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Enter" && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
event.preventDefault();
event.stopPropagation();
send();
if (event.key === Key.ENTER) {
const sendModifierIsPressed = IS_MAC ? event.metaKey : event.ctrlKey;
// if enter should send, send if the user is not pushing shift
if (enterShouldSend && !event.shiftKey) {
event.preventDefault();
event.stopPropagation();
send();
}
// if enter should not send, send only if the user is pushing ctrl/cmd
if (!enterShouldSend && sendModifierIsPressed) {
event.preventDefault();
event.stopPropagation();
send();
}
}
},
[isCtrlEnter, send],
[enterShouldSend, send],
);
return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText };

View file

@ -17,5 +17,6 @@ limitations under the License.
export {
DynamicImportSendWysiwygComposer as SendWysiwygComposer,
DynamicImportEditWysiwygComposer as EditWysiwygComposer,
dynamicImportSendMessage as sendMessage,
dynamicImportConversionFunctions as getConversionFunctions,
} from "./DynamicImportWysiwygComposer";
export { sendMessage } from "./utils/message";

View file

@ -14,13 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { richToPlain, plainToRich } from "@matrix-org/matrix-wysiwyg";
import { IContent, IEventRelation, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix";
import { htmlSerializeFromMdIfNeeded } from "../../../../../editor/serialize";
import SettingsStore from "../../../../../settings/SettingsStore";
import { RoomPermalinkCreator } from "../../../../../utils/permalinks/Permalinks";
import { addReplyToMessageContent } from "../../../../../utils/Reply";
import { htmlToPlainText } from "../../../../../utils/room/htmlToPlaintext";
// Merges favouring the given relation
function attachRelation(content: IContent, relation?: IEventRelation): void {
@ -62,7 +61,7 @@ interface CreateMessageContentParams {
editedEvent?: MatrixEvent;
}
export function createMessageContent(
export async function createMessageContent(
message: string,
isHTML: boolean,
{
@ -72,7 +71,7 @@ export function createMessageContent(
includeReplyLegacyFallback = true,
editedEvent,
}: CreateMessageContentParams,
): IContent {
): Promise<IContent> {
// TODO emote ?
const isEditing = Boolean(editedEvent);
@ -90,26 +89,22 @@ export function createMessageContent(
// const body = textSerialize(model);
// TODO remove this ugly hack for replace br tag
const body = (isHTML && htmlToPlainText(message)) || message.replace(/<br>/g, "\n");
// if we're editing rich text, the message content is pure html
// BUT if we're not, the message content will be plain text
const body = isHTML ? await richToPlain(message) : message;
const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || "";
const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || "";
const content: IContent = {
// TODO emote
msgtype: MsgType.Text,
// TODO when available, use HTML --> Plain text conversion from wysiwyg rust model
body: isEditing ? `${bodyPrefix} * ${body}` : body,
};
// TODO markdown support
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
const formattedBody = isHTML
? message
: isMarkdownEnabled
? htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply })
: null;
const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message) : null;
if (formattedBody) {
content.format = "org.matrix.custom.html";

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
import { IContent, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
@ -34,7 +34,7 @@ import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
import { createMessageContent } from "./createMessageContent";
import { isContentModified } from "./isContentModified";
interface SendMessageParams {
export interface SendMessageParams {
mxClient: MatrixClient;
relation?: IEventRelation;
replyToEvent?: MatrixEvent;
@ -43,10 +43,18 @@ interface SendMessageParams {
includeReplyLegacyFallback?: boolean;
}
export function sendMessage(message: string, isHTML: boolean, { roomContext, mxClient, ...params }: SendMessageParams) {
export async function sendMessage(
message: string,
isHTML: boolean,
{ roomContext, mxClient, ...params }: SendMessageParams,
) {
const { relation, replyToEvent } = params;
const { room } = roomContext;
const { roomId } = room;
const roomId = room?.roomId;
if (!roomId) {
return;
}
const posthogEvent: ComposerEvent = {
eventName: "Composer",
@ -63,7 +71,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
}*/
PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
let content: IContent;
const content = await createMessageContent(message, isHTML, params);
// TODO slash comment
@ -71,10 +79,6 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
// TODO quick reaction
if (!content) {
content = createMessageContent(message, isHTML, params);
}
// don't bother sending an empty message
if (!content.body.trim()) {
return;
@ -84,7 +88,7 @@ export function sendMessage(message: string, isHTML: boolean, { roomContext, mxC
decorateStartSendingTime(content);
}
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
const threadId = relation?.event_id && relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
const prom = doMaybeLocalRoomAction(
roomId,
@ -139,7 +143,7 @@ interface EditMessageParams {
editorStateTransfer: EditorStateTransfer;
}
export function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) {
export async function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) {
const editedEvent = editorStateTransfer.getEvent();
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
@ -156,7 +160,7 @@ export function editMessage(html: string, { roomContext, mxClient, editorStateTr
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
}*/
const editContent = createMessageContent(html, true, { editedEvent });
const editContent = await createMessageContent(html, true, { editedEvent });
const newContent = editContent["m.new_content"];
const shouldSend = true;
@ -174,10 +178,10 @@ export function editMessage(html: string, { roomContext, mxClient, editorStateTr
let response: Promise<ISendEventResponse> | undefined;
// If content is modified then send an updated event into the room
if (isContentModified(newContent, editorStateTransfer)) {
const roomId = editedEvent.getRoomId();
const roomId = editedEvent.getRoomId();
// If content is modified then send an updated event into the room
if (isContentModified(newContent, editorStateTransfer) && roomId) {
// TODO Slash Commands
if (shouldSend) {

View file

@ -1,34 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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 PipView from "./PipView";
interface IProps {}
interface IState {}
export default class PiPContainer extends React.PureComponent<IProps, IState> {
public render() {
return (
<div className="mx_PiPContainer">
<PipView />
</div>
);
}
}

View file

@ -19,6 +19,7 @@ import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon";
import { MatrixEvent, MatrixClient } from "matrix-js-sdk/src/matrix";
import { getShareableLocationEventForBeacon } from "../../utils/beacon/getShareableLocation";
import { VoiceBroadcastInfoEventType } from "../../voice-broadcast/types";
/**
* Get forwardable event for a given event
@ -29,6 +30,8 @@ export const getForwardableEvent = (event: MatrixEvent, cli: MatrixClient): Matr
return null;
}
if (event.getType() === VoiceBroadcastInfoEventType) return null;
// Live location beacons should forward their latest location as a static pin location
// If the beacon is not live, or doesn't have a location forwarding is not allowed
if (M_BEACON_INFO.matches(event.getType())) {

View file

@ -33,6 +33,11 @@ export const useCall = (roomId: string): Call | null => {
return call;
};
export const useCallForWidget = (widgetId: string, roomId: string): Call | null => {
const call = useCall(roomId);
return call?.widget.id === widgetId ? call : null;
};
export const useConnectionState = (call: Call): ConnectionState =>
useTypedEventEmitterState(
call,

View file

@ -255,6 +255,7 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
}
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
WidgetMessagingStore.instance.on(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
window.addEventListener("beforeunload", this.beforeUnload);
this.connectionState = ConnectionState.Connected;
}
@ -275,6 +276,7 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
*/
public setDisconnected() {
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
WidgetMessagingStore.instance.off(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
window.removeEventListener("beforeunload", this.beforeUnload);
this.messaging = null;
this.connectionState = ConnectionState.Disconnected;
@ -292,6 +294,13 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
if (membership !== "join") this.setDisconnected();
};
private onStopMessaging = (uid: string) => {
if (uid === this.widgetUid) {
logger.log("The widget died; treating this as a user hangup");
this.setDisconnected();
}
};
private beforeUnload = () => this.setDisconnected();
}

View file

@ -32,6 +32,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext";
import { launchPollEditor } from "../components/views/messages/MPollBody";
import { Action } from "../dispatcher/actions";
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../voice-broadcast/types";
/**
* Returns whether an event should allow actions like reply, reactions, edit, etc.
@ -56,7 +57,9 @@ export function isContentActionable(mxEvent: MatrixEvent): boolean {
} else if (
mxEvent.getType() === "m.sticker" ||
M_POLL_START.matches(mxEvent.getType()) ||
M_BEACON_INFO.matches(mxEvent.getType())
M_BEACON_INFO.matches(mxEvent.getType()) ||
(mxEvent.getType() === VoiceBroadcastInfoEventType &&
mxEvent.getContent()?.state === VoiceBroadcastInfoState.Started)
) {
return true;
}

View file

@ -1,19 +0,0 @@
/*
Copyright 2022 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.
*/
export function htmlToPlainText(html: string) {
return new DOMParser().parseFromString(html, "text/html").documentElement.textContent;
}