Blap: Discord-style room+call rework and Element Call swap
Ship our own Element Call fork build instead of the stock embedded package
(webpack copy repointed to ../element-call/dist, with npm-package fallback), and
rework the in-call room layout to match the Blap design:
- Chat stays in the main pane with the call as a compact right-hand column;
when someone shares a screen the call takes prominence in the main pane and
chat moves to the side column (never hidden). Driven by a same-origin
screenshare signal from the EC fork.
- Camera-off: pass the `blapVideoMuted` flag to EC and drop the old (unreliable)
widget-action mute code.
- Room list already lists in-call members under voice channels; add an
"IN THE CALL" header to the call column.
- Hide redundant RTC call-notification ("Video call") pings from the timeline.
- Room header: remove the now-redundant chat, threads and member-facepile
buttons; keep info working over the call (call stays mounted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1362,4 +1362,68 @@
|
||||
.mx_AuthFooter a {
|
||||
color: var(--yap-accent);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Blap call layout — call as a compact right-hand column
|
||||
(chat stays in the main pane; see RoomView.tsx callColumn).
|
||||
============================================================ */
|
||||
.mx_RoomView_callColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: var(--yap-surface-roomlist);
|
||||
overflow: hidden;
|
||||
}
|
||||
.mx_RoomView_callColumn .mx_CallView {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: transparent;
|
||||
}
|
||||
/* "IN THE CALL 5" header atop the call column. */
|
||||
.mx_BlapCallColumnHeader {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--yap-text-muted);
|
||||
}
|
||||
.mx_BlapCallColumnHeader_count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: var(--yap-accent-soft);
|
||||
color: var(--yap-accent);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
/* Keep the call mounted (so it doesn't disconnect) but out of view while a
|
||||
right-panel card such as Room info is open over the call column. */
|
||||
.mx_RoomView_callColumn_hidden {
|
||||
display: none;
|
||||
}
|
||||
/* When a screen share takes the main pane, chat moves here (never hidden).
|
||||
It carries mx_RoomView_body too, so the timeline/composer layout is inherited;
|
||||
these rules just frame it as a full-height column. */
|
||||
.mx_RoomView_chatColumn.mx_RoomView_body {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--yap-surface-roomlist);
|
||||
border-left: 1px solid var(--yap-hairline);
|
||||
}
|
||||
/* Give the call column a subtle divider from the chat pane. */
|
||||
.mx_MainSplit_callColumn + .mx_RightPanel_ResizeWrapper,
|
||||
.mx_RoomView_callColumn {
|
||||
border-left: 1px solid var(--yap-hairline);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from "../../effects/utils";
|
||||
import { CHAT_EFFECTS } from "../../effects";
|
||||
import { CallView } from "../views/voip/CallView";
|
||||
import { BlapCallColumnHeader } from "../views/voip/BlapCallColumnHeader";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
|
||||
@@ -234,6 +235,9 @@ export interface IRoomState {
|
||||
*/
|
||||
search?: SearchInfo;
|
||||
callState?: CallState;
|
||||
// Blap: true when someone in this room's call is screen sharing (signalled from
|
||||
// our Element Call fork via localStorage). Drives the call-takes-prominence layout.
|
||||
callScreenSharing?: boolean;
|
||||
canPeek: boolean;
|
||||
canSelfRedact: boolean;
|
||||
showApps: boolean;
|
||||
@@ -478,6 +482,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
membersLoaded: !llMembers,
|
||||
numUnreadMessages: 0,
|
||||
callState: undefined,
|
||||
callScreenSharing: false,
|
||||
canPeek: false,
|
||||
canSelfRedact: false,
|
||||
showApps: false,
|
||||
@@ -647,6 +652,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
const newState: Partial<IRoomState> = {
|
||||
roomId: roomId ?? undefined,
|
||||
// Blap: pick up any in-progress screenshare for the room we're switching to
|
||||
// (storage events only fire on change, so read the current value directly).
|
||||
callScreenSharing: this.readBlapScreenSharing(roomId),
|
||||
roomAlias: roomAlias,
|
||||
roomLoading: roomLoading,
|
||||
roomLoadError,
|
||||
@@ -1032,8 +1040,31 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
this.context.legacyCallHandler.on(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||
window.addEventListener("beforeunload", this.onPageUnload);
|
||||
// Blap: listen for screenshare state broadcast from our Element Call fork.
|
||||
window.addEventListener("storage", this.onBlapScreenShareStorage);
|
||||
}
|
||||
|
||||
// Blap: read whether someone is screen sharing in the given room's call.
|
||||
private readBlapScreenSharing(roomId?: string | null): boolean {
|
||||
if (!roomId) return false;
|
||||
try {
|
||||
return localStorage.getItem(`blap:screenshare:${roomId}`) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private onBlapScreenShareStorage = (e: StorageEvent): void => {
|
||||
const roomId = this.state.roomId;
|
||||
if (!roomId) return;
|
||||
// e.key is null when storage is cleared wholesale; otherwise filter to our key.
|
||||
if (e.key !== null && e.key !== `blap:screenshare:${roomId}`) return;
|
||||
const sharing = this.readBlapScreenSharing(roomId);
|
||||
if (sharing !== Boolean(this.state.callScreenSharing)) {
|
||||
this.setState({ callScreenSharing: sharing });
|
||||
}
|
||||
};
|
||||
|
||||
public shouldComponentUpdate(nextProps: IRoomProps, nextState: IRoomState): boolean {
|
||||
const hasPropsDiff = objectHasDiff(this.props, nextProps);
|
||||
|
||||
@@ -1097,6 +1128,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
}
|
||||
|
||||
window.removeEventListener("beforeunload", this.onPageUnload);
|
||||
window.removeEventListener("storage", this.onBlapScreenShareStorage);
|
||||
|
||||
this.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
@@ -2648,29 +2680,39 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
const showChatEffects = SettingsStore.getValue("showChatEffects");
|
||||
|
||||
// Blap: the chat timeline body, reused both for normal Timeline mode and for
|
||||
// the call layout (where chat stays in the main pane and the call moves to a
|
||||
// compact right-hand column).
|
||||
const timelineBody = (
|
||||
<RoomUploadContextProvider>
|
||||
<Measured sensor={this.roomViewBody} onMeasurement={this.onMeasurement} />
|
||||
{auxPanel}
|
||||
{pinnedMessageBanner}
|
||||
<main className={timelineClasses} data-testid="timeline">
|
||||
<FileDropTarget parent={this.roomView.current} />
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
{searchResultsPanel}
|
||||
</main>
|
||||
{statusBarArea}
|
||||
{previewBar}
|
||||
{messageComposer}
|
||||
</RoomUploadContextProvider>
|
||||
);
|
||||
|
||||
let mainSplitBody: JSX.Element | undefined;
|
||||
let mainSplitContentClassName: string | undefined;
|
||||
// Blap: the call and chat swap between the main pane and a right-hand column
|
||||
// depending on screenshare prominence — but chat is never hidden, it just moves
|
||||
// to the column. Exactly one of these is set in call mode.
|
||||
let callColumn: JSX.Element | undefined;
|
||||
let chatColumn: JSX.Element | undefined;
|
||||
// Decide what to show in the main split
|
||||
switch (mainSplitContentType) {
|
||||
case MainSplitContentType.Timeline:
|
||||
mainSplitContentClassName = "mx_MainSplit_timeline";
|
||||
mainSplitBody = (
|
||||
<RoomUploadContextProvider>
|
||||
<Measured sensor={this.roomViewBody} onMeasurement={this.onMeasurement} />
|
||||
{auxPanel}
|
||||
{pinnedMessageBanner}
|
||||
<main className={timelineClasses} data-testid="timeline">
|
||||
<FileDropTarget parent={this.roomView.current} />
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
{searchResultsPanel}
|
||||
</main>
|
||||
{statusBarArea}
|
||||
{previewBar}
|
||||
{messageComposer}
|
||||
</RoomUploadContextProvider>
|
||||
);
|
||||
mainSplitBody = timelineBody;
|
||||
break;
|
||||
case MainSplitContentType.MaximisedWidget:
|
||||
mainSplitContentClassName = "mx_MainSplit_maximisedWidget";
|
||||
@@ -2687,18 +2729,45 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
);
|
||||
break;
|
||||
case MainSplitContentType.Call: {
|
||||
mainSplitContentClassName = "mx_MainSplit_call";
|
||||
mainSplitBody = (
|
||||
<>
|
||||
// Blap: give the call the main pane while someone is sharing a screen;
|
||||
// otherwise it stays a compact column so the chat keeps the main pane.
|
||||
// (Our Element Call fork reliably clears the screenshare flag when the
|
||||
// call is left, so no separate connection check is needed.)
|
||||
if (this.state.callScreenSharing) {
|
||||
// Blap: someone is screen sharing → the call takes prominence in the
|
||||
// main pane, and the chat moves to the right-hand column (never hidden).
|
||||
mainSplitContentClassName = "mx_MainSplit_call";
|
||||
mainSplitBody = (
|
||||
<CallView
|
||||
room={this.state.room}
|
||||
resizing={this.state.resizing}
|
||||
role="main"
|
||||
onClose={this.onCallClose}
|
||||
/>
|
||||
{previewBar}
|
||||
</>
|
||||
);
|
||||
);
|
||||
// Reuse mx_RoomView_body so the timeline fills and the composer anchors
|
||||
// to the bottom exactly like the main pane; mx_RoomView_chatColumn just
|
||||
// adds the column framing.
|
||||
chatColumn = (
|
||||
<div className="mx_RoomView_body mx_RoomView_chatColumn">{timelineBody}</div>
|
||||
);
|
||||
} else {
|
||||
// Blap (Discord-style): keep chat in the main pane, render the call as
|
||||
// a compact right-hand column instead of a full-pane takeover.
|
||||
mainSplitContentClassName = "mx_MainSplit_call mx_MainSplit_callColumn";
|
||||
mainSplitBody = timelineBody;
|
||||
callColumn = (
|
||||
<div className="mx_RoomView_callColumn">
|
||||
<BlapCallColumnHeader room={this.state.room} />
|
||||
<CallView
|
||||
room={this.state.room}
|
||||
resizing={this.state.resizing}
|
||||
role="complementary"
|
||||
onClose={this.onCallClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const mainSplitContentClasses = classNames("mx_RoomView_body", mainSplitContentClassName);
|
||||
@@ -2714,6 +2783,26 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
this.state.mainSplitContentType === MainSplitContentType.Call ? "video_room" : "maximised_widget";
|
||||
}
|
||||
|
||||
// Blap: work out what actually goes in the resizable side panel.
|
||||
// - Normally the call column (or, during a screen share, the chat column).
|
||||
// - But if the user opens a right-panel card (e.g. Room info) while a call
|
||||
// column is showing, reveal that card instead — keeping the call mounted but
|
||||
// hidden so it doesn't disconnect. Without this the info panel never renders,
|
||||
// because the call column would otherwise always own the panel slot.
|
||||
let panelNode: JSX.Element | undefined = callColumn ?? chatColumn ?? rightPanel;
|
||||
let panelSizeKey = callColumn ? "blap-call" : chatColumn ? "blap-chat" : sizeKey;
|
||||
let panelDefaultSize = callColumn ? 360 : chatColumn ? 400 : defaultSize;
|
||||
if ((callColumn || chatColumn) && showRightPanel && rightPanel) {
|
||||
panelNode = (
|
||||
<>
|
||||
<div className="mx_RoomView_callColumn_hidden">{callColumn ?? chatColumn}</div>
|
||||
{rightPanel}
|
||||
</>
|
||||
);
|
||||
panelSizeKey = undefined;
|
||||
panelDefaultSize = undefined;
|
||||
}
|
||||
|
||||
const extraButtons: JSX.Element[] = [];
|
||||
for (const cb of ModuleApi.instance.extras.roomHeaderButtonsCallbacks) {
|
||||
const b = cb(this.state.room.roomId);
|
||||
@@ -2734,9 +2823,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
)}
|
||||
<ErrorBoundary>
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
sizeKey={sizeKey}
|
||||
defaultSize={defaultSize}
|
||||
panel={panelNode}
|
||||
sizeKey={panelSizeKey}
|
||||
defaultSize={panelDefaultSize}
|
||||
analyticsRoomType={analyticsRoomType}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -12,7 +12,6 @@ import { Text, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/co
|
||||
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid";
|
||||
import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid";
|
||||
import CloseCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
|
||||
import ThreadsIcon from "@vector-im/compound-design-tokens/assets/web/icons/threads-solid";
|
||||
import RoomInfoIcon from "@vector-im/compound-design-tokens/assets/web/icons/info-solid";
|
||||
import NotificationsIcon from "@vector-im/compound-design-tokens/assets/web/icons/notifications-solid";
|
||||
import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
|
||||
@@ -28,32 +27,23 @@ import { useRoomName } from "../../../../hooks/useRoomName.ts";
|
||||
import { useTopic } from "../../../../hooks/room/useTopic.ts";
|
||||
import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases.ts";
|
||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx";
|
||||
import { useRoomMemberCount, useRoomMembers } from "../../../../hooks/useRoomMembers.ts";
|
||||
import { _t } from "../../../../languageHandler.tsx";
|
||||
import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx";
|
||||
import { useRoomThreadNotifications } from "../../../../hooks/room/useRoomThreadNotifications.ts";
|
||||
import { useGlobalNotificationState } from "../../../../hooks/useGlobalNotificationState.ts";
|
||||
import { useFeatureEnabled } from "../../../../hooks/useSettings.ts";
|
||||
import { useEncryptionStatus } from "../../../../hooks/useEncryptionStatus.ts";
|
||||
import { E2EStatus } from "../../../../utils/ShieldUtils.ts";
|
||||
import FacePile from "../../elements/FacePile.tsx";
|
||||
import { useRoomState } from "../../../../hooks/useRoomState.ts";
|
||||
import RoomAvatar from "../../avatars/RoomAvatar.tsx";
|
||||
import { formatCount } from "../../../../utils/FormattingUtils.ts";
|
||||
import RightPanelStore from "../../../../stores/right-panel/RightPanelStore.ts";
|
||||
import PosthogTrackers from "../../../../PosthogTrackers.ts";
|
||||
import { VideoRoomChatButton } from "./VideoRoomChatButton.tsx";
|
||||
import { RoomKnocksBar } from "../RoomKnocksBar.tsx";
|
||||
import { isVideoRoom as calcIsVideoRoom } from "../../../../utils/video-rooms.ts";
|
||||
import { notificationLevelToIndicator } from "../../../../utils/notifications.ts";
|
||||
import { CallGuestLinkButton } from "./CallGuestLinkButton.tsx";
|
||||
import { type ButtonEvent } from "../../elements/AccessibleButton.tsx";
|
||||
import WithPresenceIndicator, { useDmMember } from "../../avatars/WithPresenceIndicator.tsx";
|
||||
import { type IOOBData } from "../../../../stores/ThreepidInviteStore.ts";
|
||||
import { MainSplitContentType } from "../../../structures/RoomView.tsx";
|
||||
import defaultDispatcher from "../../../../dispatcher/dispatcher.ts";
|
||||
import { RoomSettingsTab } from "../../dialogs/RoomSettingsDialog.tsx";
|
||||
import { useScopedRoomContext } from "../../../../contexts/ScopedRoomContext.tsx";
|
||||
import { ToggleableIcon } from "./toggle/ToggleableIcon.tsx";
|
||||
import { CurrentRightPanelPhaseContextProvider } from "../../../../contexts/CurrentRightPanelPhaseContext.tsx";
|
||||
import { LocalRoom } from "../../../../models/LocalRoom.ts";
|
||||
@@ -68,9 +58,6 @@ function RoomHeaderButtons({
|
||||
legacyAdditionalButtons?: ViewRoomOpts["buttons"];
|
||||
extraButtons?: JSX.Element;
|
||||
}): JSX.Element {
|
||||
const members = useRoomMembers(room, 2500);
|
||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500, includeInvited: true });
|
||||
|
||||
const {
|
||||
voiceCallDisabledReason,
|
||||
voiceCallClick,
|
||||
@@ -84,11 +71,8 @@ function RoomHeaderButtons({
|
||||
showVoiceCallButton,
|
||||
showVideoCallButton,
|
||||
} = useRoomCall(room);
|
||||
const threadNotifications = useRoomThreadNotifications(room);
|
||||
const globalNotificationState = useGlobalNotificationState();
|
||||
|
||||
const dmMember = useDmMember(room);
|
||||
const isDirectMessage = !!dmMember;
|
||||
|
||||
const notificationsEnabled = useFeatureEnabled("feature_notifications");
|
||||
|
||||
@@ -292,12 +276,7 @@ function RoomHeaderButtons({
|
||||
voiceCallButton = undefined;
|
||||
}
|
||||
|
||||
const roomContext = useScopedRoomContext("mainSplitContentType");
|
||||
const isVideoRoom = calcIsVideoRoom(room);
|
||||
const showChatButton =
|
||||
isVideoRoom ||
|
||||
roomContext.mainSplitContentType === MainSplitContentType.MaximisedWidget ||
|
||||
roomContext.mainSplitContentType === MainSplitContentType.Call;
|
||||
return (
|
||||
<>
|
||||
{extraButtons}
|
||||
@@ -331,21 +310,8 @@ function RoomHeaderButtons({
|
||||
</>
|
||||
)}
|
||||
|
||||
{showChatButton && <VideoRoomChatButton room={room} />}
|
||||
|
||||
<Tooltip label={_t("common|threads")}>
|
||||
<IconButton
|
||||
indicator={notificationLevelToIndicator(threadNotifications)}
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel);
|
||||
PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt);
|
||||
}}
|
||||
aria-label={_t("common|threads")}
|
||||
>
|
||||
<ToggleableIcon Icon={ThreadsIcon} phase={RightPanelPhases.ThreadPanel} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/* Blap: chat button is redundant (chat is always in the main pane), and the
|
||||
threads button isn't wanted. Both removed. */}
|
||||
{notificationsEnabled && (
|
||||
<Tooltip label={_t("notifications|enable_prompt_toast_title")}>
|
||||
<IconButton
|
||||
@@ -373,25 +339,7 @@ function RoomHeaderButtons({
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
{!isDirectMessage && (
|
||||
<Text as="div" size="sm" weight="medium">
|
||||
<FacePile
|
||||
className="mx_RoomHeader_members"
|
||||
members={members.slice(0, 3)}
|
||||
size="20px"
|
||||
overflow={false}
|
||||
viewUserOnClick={false}
|
||||
tooltipLabel={_t("room|header_face_pile_tooltip")}
|
||||
onClick={(e: ButtonEvent) => {
|
||||
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
aria-label={_t("common|n_members", { count: memberCount })}
|
||||
>
|
||||
{formatCount(memberCount)}
|
||||
</FacePile>
|
||||
</Text>
|
||||
)}
|
||||
{/* Blap: the member facepile ("who's in the room") is removed from the header. */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2026 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useCall, useParticipatingMembers } from "../../../hooks/useCall";
|
||||
|
||||
interface Props {
|
||||
room: Room;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blap: the "IN THE CALL" header shown at the top of the call column in the room
|
||||
* view, with a live participant count. Reuses the same MatrixRTC participation data
|
||||
* as the room-list voice-member rows, so it reflects who is actually in the call.
|
||||
*/
|
||||
export function BlapCallColumnHeader({ room }: Props): JSX.Element | null {
|
||||
const call = useCall(room.roomId);
|
||||
const members = useParticipatingMembers(call);
|
||||
if (members.length === 0) return null;
|
||||
return (
|
||||
<div className="mx_BlapCallColumnHeader">
|
||||
<span className="mx_BlapCallColumnHeader_label">In the call</span>
|
||||
<span className="mx_BlapCallColumnHeader_count">{members.length}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -764,6 +764,15 @@ export class ElementCall extends Call {
|
||||
if (typeof opts.skipLobby === "boolean") {
|
||||
params.set("skipLobby", opts.skipLobby.toString());
|
||||
}
|
||||
// Blap: drop Element Call's own header — element-web draws its own room header,
|
||||
// so EC's is redundant chrome. Step toward the compact call-column layout.
|
||||
params.set("header", "none");
|
||||
// Blap: tell our Element Call fork to start with the camera off when the user
|
||||
// has opted in (Settings -> Voice & Video). Read by calculateInitialMuteState
|
||||
// in the EC build; the widget-action mute channel is unreliable across versions.
|
||||
if (SettingsStore.getValue("blapJoinCameraOff")) {
|
||||
params.set("blapVideoMuted", "true");
|
||||
}
|
||||
|
||||
const rageshakeSubmitUrl = SdkConfig.get("bug_report_endpoint_url");
|
||||
if (rageshakeSubmitUrl && rageshakeSubmitUrl !== BugReportEndpointURLLocal) {
|
||||
@@ -894,6 +903,7 @@ export class ElementCall extends Call {
|
||||
// Some parameters may only be set once the user has chosen to interact with the call, regenerate the URL
|
||||
// at this point in case any of the parameters have changed.
|
||||
this.widgetGenerationParameters = { ...this.widgetGenerationParameters, ...widgetGenerationParameters };
|
||||
|
||||
this.widget.url = ElementCall.generateWidgetUrl(
|
||||
this.client,
|
||||
this.roomId,
|
||||
@@ -984,14 +994,8 @@ export class ElementCall extends Call {
|
||||
ev.preventDefault();
|
||||
this.widgetApi!.transport.reply(ev.detail, {}); // ack
|
||||
this.setConnected();
|
||||
// Blap: optionally join with the camera muted. EC accepts a toWidget
|
||||
// device_mute request ({ audio_enabled?, video_enabled? }); undefined
|
||||
// fields are left as-is, so this only forces video off.
|
||||
if (SettingsStore.getValue("blapJoinCameraOff")) {
|
||||
this.widgetApi!.transport.send(ElementWidgetActions.DeviceMute, { video_enabled: false }).catch((e) => {
|
||||
console.warn("Blap: failed to mute camera on join", e);
|
||||
});
|
||||
}
|
||||
// Blap: joining with the camera off is handled inside our Element Call fork
|
||||
// (calculateInitialMuteState, via the blapVideoMuted URL flag). Nothing to do here.
|
||||
};
|
||||
|
||||
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
|
||||
@@ -53,6 +53,11 @@ export default function shouldHideEvent(ev: MatrixEvent, ctx?: IRoomState): bool
|
||||
// Hide all poll end events
|
||||
if (M_POLL_END.matches(ev.getType())) return true;
|
||||
|
||||
// Blap: the call is always visible as a right-hand column, so the per-call
|
||||
// "Video call" notification pings (MSC4075 rtc.notification) in the timeline are
|
||||
// redundant noise — hide them.
|
||||
if (ev.getType() === EventType.RTCNotification) return true;
|
||||
|
||||
// Accessing the settings store directly can be expensive if done frequently,
|
||||
// so we should prefer using cached values if a RoomContext is available
|
||||
const isEnabled = ctx
|
||||
|
||||
@@ -722,10 +722,16 @@ export default (env: string, argv: Record<string, any>): webpack.Configuration =
|
||||
{ from: "decoder-ring/**", context: path.resolve(__dirname, "res") },
|
||||
{ from: "media/**", context: path.resolve(__dirname, "res/") },
|
||||
{ from: "config.json", noErrorOnMissing: true },
|
||||
// Element Call embedded widget
|
||||
// Element Call embedded widget.
|
||||
// Blap: use our own Element Call fork build (sibling checkout) instead of the
|
||||
// stock @element-hq/element-call-embedded package, so we control the call UI
|
||||
// (reskin) and behaviour (e.g. join-with-camera-off). Falls back to the npm
|
||||
// package if the fork build isn't present.
|
||||
{
|
||||
from: "**",
|
||||
context: path.join(getPackageRoot("@element-hq/element-call-embedded"), "dist"),
|
||||
context: fs.existsSync(path.resolve(__dirname, "../../../element-call/dist"))
|
||||
? path.resolve(__dirname, "../../../element-call/dist")
|
||||
: path.join(getPackageRoot("@element-hq/element-call-embedded"), "dist"),
|
||||
to: path.join(__dirname, "webapp", "widgets", "element-call"),
|
||||
},
|
||||
// Mobile guide assets
|
||||
|
||||
Reference in New Issue
Block a user