Merge branch 'develop' into travis/remove-skinning
This commit is contained in:
commit
4057833036
74 changed files with 1412 additions and 1717 deletions
|
@ -40,7 +40,6 @@ import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
|||
import IndicatorScrollbar from "./IndicatorScrollbar";
|
||||
import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import VoiceChannelRadio from "../views/voip/VoiceChannelRadio";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../settings/UIFeature";
|
||||
|
@ -439,7 +438,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
{ roomList }
|
||||
</div>
|
||||
</div>
|
||||
{ SettingsStore.getValue("feature_voice_rooms") && <VoiceChannelRadio /> }
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -31,6 +31,7 @@ import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { throttle } from "lodash";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by various components
|
||||
import 'focus-visible';
|
||||
|
@ -676,7 +677,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
break;
|
||||
}
|
||||
case 'view_create_room':
|
||||
this.createRoom(payload.public, payload.defaultName);
|
||||
this.createRoom(payload.public, payload.defaultName, payload.type);
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this.viewSomethingBehindModal();
|
||||
|
@ -993,8 +994,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.setPage(PageType.LegacyGroupView);
|
||||
}
|
||||
|
||||
private async createRoom(defaultPublic = false, defaultName?: string) {
|
||||
private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType) {
|
||||
const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
|
||||
type,
|
||||
defaultPublic,
|
||||
defaultName,
|
||||
});
|
||||
|
|
|
@ -74,7 +74,7 @@ import EffectsOverlay from "../views/elements/EffectsOverlay";
|
|||
import { containsEmoji } from '../../effects/utils';
|
||||
import { CHAT_EFFECTS } from '../../effects';
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import { getVoiceChannel } from "../../utils/VoiceChannelUtils";
|
||||
import { getVideoChannel } from "../../utils/VideoChannelUtils";
|
||||
import AppTile from "../views/elements/AppTile";
|
||||
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
|
@ -373,7 +373,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private getMainSplitContentType = (room: Room) => {
|
||||
if (SettingsStore.getValue("feature_voice_rooms") && room.isCallRoom()) {
|
||||
if (SettingsStore.getValue("feature_video_rooms") && room.isElementVideoRoom()) {
|
||||
return MainSplitContentType.Video;
|
||||
}
|
||||
if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
|
||||
|
@ -942,6 +942,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
if (ev.getType() === "m.room.encryption") {
|
||||
this.updateE2EStatus(room);
|
||||
this.updatePreviewUrlVisibility(room);
|
||||
}
|
||||
|
||||
// ignore anything but real-time updates at the end of the room:
|
||||
|
@ -2097,6 +2098,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
const mainClasses = classNames("mx_RoomView", {
|
||||
mx_RoomView_inCall: Boolean(activeCall),
|
||||
mx_RoomView_immersive: this.state.mainSplitContentType === MainSplitContentType.Video,
|
||||
});
|
||||
|
||||
const showChatEffects = SettingsStore.getValue('showChatEffects');
|
||||
|
@ -2138,7 +2140,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
</>;
|
||||
break;
|
||||
case MainSplitContentType.Video: {
|
||||
const app = getVoiceChannel(this.state.room.roomId);
|
||||
const app = getVideoChannel(this.state.room.roomId);
|
||||
if (!app) break;
|
||||
mainSplitContentClassName = "mx_MainSplit_video";
|
||||
mainSplitBody = <AppTile
|
||||
|
@ -2155,19 +2157,32 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
const mainSplitContentClasses = classNames("mx_RoomView_body", mainSplitContentClassName);
|
||||
|
||||
let excludedRightPanelPhaseButtons = [RightPanelPhases.Timeline];
|
||||
let onCallPlaced = this.onCallPlaced;
|
||||
let onAppsClick = this.onAppsClick;
|
||||
let onForgetClick = this.onForgetClick;
|
||||
let onSearchClick = this.onSearchClick;
|
||||
if (this.state.mainSplitContentType !== MainSplitContentType.Timeline) {
|
||||
// Disable phase buttons and action button to have a simplified header
|
||||
// and enable (not disable) the RightPanelPhases.Timeline button
|
||||
excludedRightPanelPhaseButtons = [
|
||||
RightPanelPhases.ThreadPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
];
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
|
||||
// Simplify the header for other main split types
|
||||
switch (this.state.mainSplitContentType) {
|
||||
case MainSplitContentType.MaximisedWidget:
|
||||
excludedRightPanelPhaseButtons = [
|
||||
RightPanelPhases.ThreadPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
];
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
break;
|
||||
case MainSplitContentType.Video:
|
||||
excludedRightPanelPhaseButtons = [
|
||||
RightPanelPhases.ThreadPanel,
|
||||
RightPanelPhases.PinnedMessages,
|
||||
RightPanelPhases.NotificationPanel,
|
||||
];
|
||||
onCallPlaced = null;
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -2187,7 +2202,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
e2eStatus={this.state.e2eStatus}
|
||||
onAppsClick={this.state.hasPinnedWidgets ? onAppsClick : null}
|
||||
appsShown={this.state.showApps}
|
||||
onCallPlaced={this.onCallPlaced}
|
||||
onCallPlaced={onCallPlaced}
|
||||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||
/>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { RefObject, useContext, useRef, useState } from "react";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -29,6 +29,7 @@ import RoomTopic from "../views/elements/RoomTopic";
|
|||
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||
import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
|
||||
import { useRoomMembers } from "../../hooks/useRoomMembers";
|
||||
import { useFeatureEnabled } from "../../hooks/useSettings";
|
||||
import createRoom, { IOpts } from "../../createRoom";
|
||||
import Field from "../views/elements/Field";
|
||||
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
|
||||
|
@ -57,7 +58,7 @@ import {
|
|||
} from "../../utils/space";
|
||||
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
|
||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||
import { RoomFacePile } from "../views/elements/FacePile";
|
||||
import FacePile from "../views/elements/FacePile";
|
||||
import {
|
||||
AddExistingToSpace,
|
||||
defaultDmsRenderer,
|
||||
|
@ -297,7 +298,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }: ISp
|
|||
</div>
|
||||
}
|
||||
</RoomTopic>
|
||||
{ space.getJoinRule() === "public" && <RoomFacePile room={space} /> }
|
||||
{ space.getJoinRule() === "public" && <FacePile room={space} /> }
|
||||
<div className="mx_SpaceRoomView_preview_joinButtons">
|
||||
{ joinButtons }
|
||||
</div>
|
||||
|
@ -309,6 +310,7 @@ const SpaceLandingAddButton = ({ space }) => {
|
|||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||
const canCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
|
||||
const canCreateSpace = shouldShowComponent(UIComponent.CreateSpaces);
|
||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
|
||||
let contextMenu;
|
||||
if (menuDisplayed) {
|
||||
|
@ -322,20 +324,35 @@ const SpaceLandingAddButton = ({ space }) => {
|
|||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ canCreateRoom && <IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
{ canCreateRoom && <>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
PosthogTrackers.trackInteraction("WebSpaceHomeCreateRoomButton", e);
|
||||
if (await showCreateNewRoom(space)) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
}}
|
||||
/> }
|
||||
PosthogTrackers.trackInteraction("WebSpaceHomeCreateRoomButton", e);
|
||||
if (await showCreateNewRoom(space)) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
|
||||
if (await showCreateNewRoom(space, RoomType.ElementVideo)) {
|
||||
defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
|
||||
}
|
||||
}}
|
||||
/> }
|
||||
</> }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconAddExistingRoom"
|
||||
|
@ -437,7 +454,7 @@ const SpaceLanding = ({ space }: { space: Room }) => {
|
|||
<div className="mx_SpaceRoomView_landing_infoBar">
|
||||
<SpaceInfo space={space} />
|
||||
<div className="mx_SpaceRoomView_landing_infoBar_interactive">
|
||||
<RoomFacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||
{ inviteButton }
|
||||
{ settingsButton }
|
||||
</div>
|
||||
|
|
|
@ -31,7 +31,14 @@ import { Layout } from '../../settings/enums/Layout';
|
|||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||
import Measured from '../views/elements/Measured';
|
||||
import PosthogTrackers from "../../PosthogTrackers";
|
||||
import { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import { BetaPill } from '../views/beta/BetaCard';
|
||||
import SdkConfig from '../../SdkConfig';
|
||||
import Modal from '../../Modal';
|
||||
import BetaFeedbackDialog from '../views/dialogs/BetaFeedbackDialog';
|
||||
import { Action } from '../../dispatcher/actions';
|
||||
import { UserTab } from '../views/dialogs/UserSettingsDialog';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
|
||||
interface IProps {
|
||||
roomId: string;
|
||||
|
@ -101,7 +108,7 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption, empty }: {
|
|||
isSelected={opt === value}
|
||||
/>);
|
||||
const contextMenu = menuDisplayed ? <ContextMenu
|
||||
top={100}
|
||||
top={108}
|
||||
right={33}
|
||||
onFinished={closeMenu}
|
||||
chevronFace={ChevronFace.Top}
|
||||
|
@ -129,25 +136,44 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption, empty }: {
|
|||
};
|
||||
|
||||
interface EmptyThreadIProps {
|
||||
hasThreads: boolean;
|
||||
filterOption: ThreadFilterType;
|
||||
showAllThreadsCallback: () => void;
|
||||
}
|
||||
|
||||
const EmptyThread: React.FC<EmptyThreadIProps> = ({ filterOption, showAllThreadsCallback }) => {
|
||||
const EmptyThread: React.FC<EmptyThreadIProps> = ({ hasThreads, filterOption, showAllThreadsCallback }) => {
|
||||
let body: JSX.Element;
|
||||
if (hasThreads) {
|
||||
body = <>
|
||||
<p>
|
||||
{ _t("Reply to an ongoing thread or use “%(replyInThread)s” "
|
||||
+ "when hovering over a message to start a new one.", {
|
||||
replyInThread: _t("Reply in thread"),
|
||||
}) }
|
||||
</p>
|
||||
<p>
|
||||
{ /* Always display that paragraph to prevent layout shift when hiding the button */ }
|
||||
{ (filterOption === ThreadFilterType.My)
|
||||
? <button onClick={showAllThreadsCallback}>{ _t("Show all threads") }</button>
|
||||
: <> </>
|
||||
}
|
||||
</p>
|
||||
</>;
|
||||
} else {
|
||||
body = <>
|
||||
<p>{ _t("Threads help keep your conversations on-topic and easy to track.") }</p>
|
||||
<p className="mx_ThreadPanel_empty_tip">
|
||||
{ _t('<b>Tip:</b> Use "Reply in thread" when hovering over a message.', {}, {
|
||||
b: sub => <b>{ sub }</b>,
|
||||
}) }
|
||||
</p>
|
||||
</>;
|
||||
}
|
||||
|
||||
return <aside className="mx_ThreadPanel_empty">
|
||||
<div className="mx_ThreadPanel_largeIcon" />
|
||||
<h2>{ _t("Keep discussions organised with threads") }</h2>
|
||||
<p>{ _t("Reply to an ongoing thread or use “%(replyInThread)s” "
|
||||
+ "when hovering over a message to start a new one.", { replyInThread: _t("Reply in thread") }) }
|
||||
</p>
|
||||
<p>
|
||||
{ /* Always display that paragraph to prevent layout shift
|
||||
When hiding the button */ }
|
||||
{ filterOption === ThreadFilterType.My
|
||||
? <button onClick={showAllThreadsCallback}>{ _t("Show all threads") }</button>
|
||||
: <> </>
|
||||
}
|
||||
</p>
|
||||
{ body }
|
||||
</aside>;
|
||||
};
|
||||
|
||||
|
@ -214,6 +240,12 @@ const ThreadPanel: React.FC<IProps> = ({
|
|||
}
|
||||
}, [timelineSet, timelinePanel]);
|
||||
|
||||
const openFeedback = SdkConfig.get().bug_report_endpoint_url ? () => {
|
||||
Modal.createTrackedDialog("Threads Feedback", "feature_thread", BetaFeedbackDialog, {
|
||||
featureId: "feature_thread",
|
||||
});
|
||||
} : null;
|
||||
|
||||
return (
|
||||
<RoomContext.Provider value={{
|
||||
...roomContext,
|
||||
|
@ -227,6 +259,22 @@ const ThreadPanel: React.FC<IProps> = ({
|
|||
setFilterOption={setFilterOption}
|
||||
empty={threadCount === 0}
|
||||
/>}
|
||||
footer={<>
|
||||
<BetaPill
|
||||
tooltipTitle={_t("Threads are a beta feature")}
|
||||
tooltipCaption={_t("Click for more info")}
|
||||
onClick={() => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{ openFeedback && _t("<a>Give feedback</a>", {}, {
|
||||
a: sub =>
|
||||
<AccessibleButton kind="link_inline" onClick={openFeedback}>{ sub }</AccessibleButton>,
|
||||
}) }
|
||||
</>}
|
||||
className="mx_ThreadPanel"
|
||||
onClose={onClose}
|
||||
withoutScrollContainer={true}
|
||||
|
@ -238,6 +286,7 @@ const ThreadPanel: React.FC<IProps> = ({
|
|||
/>
|
||||
{ timelineSet && (
|
||||
<TimelinePanel
|
||||
key={timelineSet.getFilter().filterId}
|
||||
ref={timelinePanel}
|
||||
showReadReceipts={false} // No RR support in thread's MVP
|
||||
manageReadReceipts={false} // No RR support in thread's MVP
|
||||
|
@ -246,6 +295,7 @@ const ThreadPanel: React.FC<IProps> = ({
|
|||
timelineSet={timelineSet}
|
||||
showUrlPreview={false} // No URL previews at the threads list level
|
||||
empty={<EmptyThread
|
||||
hasThreads={room.threadsTimelineSets?.[0]?.getLiveTimeline().getEvents().length > 0}
|
||||
filterOption={filterOption}
|
||||
showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)}
|
||||
/>}
|
||||
|
|
|
@ -36,17 +36,27 @@ interface IProps {
|
|||
featureId: string;
|
||||
}
|
||||
|
||||
export const BetaPill = ({ onClick }: { onClick?: () => void }) => {
|
||||
interface IBetaPillProps {
|
||||
onClick?: () => void;
|
||||
tooltipTitle?: string;
|
||||
tooltipCaption?: string;
|
||||
}
|
||||
|
||||
export const BetaPill = ({
|
||||
onClick,
|
||||
tooltipTitle = _t("This is a beta feature"),
|
||||
tooltipCaption = _t("Click for more info"),
|
||||
}: IBetaPillProps) => {
|
||||
if (onClick) {
|
||||
return <AccessibleTooltipButton
|
||||
className="mx_BetaCard_betaPill"
|
||||
title={_t("This is a beta feature. Click for more info")}
|
||||
title={`${tooltipTitle} ${tooltipCaption}`}
|
||||
tooltip={<div>
|
||||
<div className="mx_Tooltip_title">
|
||||
{ _t("This is a beta feature") }
|
||||
{ tooltipTitle }
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{ _t("Click for more info") }
|
||||
{ tooltipCaption }
|
||||
</div>
|
||||
</div>}
|
||||
onClick={onClick}
|
||||
|
|
|
@ -41,7 +41,6 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
|||
import { ChevronFace, IPosition } from '../../structures/ContextMenu';
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore';
|
||||
import EndPollDialog from '../dialogs/EndPollDialog';
|
||||
import { isPollEnded } from '../messages/MPollBody';
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
|
@ -471,14 +470,11 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||
timelineRenderingType === TimelineRenderingType.Thread ||
|
||||
timelineRenderingType === TimelineRenderingType.ThreadsList
|
||||
);
|
||||
const isThreadRootEvent = isThread && this.props.mxEvent?.getThread()?.rootEvent === this.props.mxEvent;
|
||||
const isThreadRootEvent = isThread && this.props.mxEvent.isThreadRoot;
|
||||
|
||||
const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget(
|
||||
MatrixClientPeg.get().getRoom(mxEvent.getRoomId()),
|
||||
);
|
||||
const commonItemsList = (
|
||||
<IconizedContextMenuOptionList>
|
||||
{ (isThreadRootEvent && isMainSplitTimelineShown) && <IconizedContextMenuOption
|
||||
{ isThreadRootEvent && <IconizedContextMenuOption
|
||||
iconClassName="mx_MessageContextMenu_iconViewInRoom"
|
||||
label={_t("View in room")}
|
||||
onClick={this.viewInRoom}
|
||||
|
|
|
@ -35,7 +35,7 @@ import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
|
|||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import Modal from "../../../Modal";
|
||||
import ExportDialog from "../dialogs/ExportDialog";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { usePinnedEvents } from "../right_panel/PinnedMessagesCard";
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
|
@ -105,6 +105,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
|||
}
|
||||
|
||||
const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||
const isVideoRoom = useFeatureEnabled("feature_video_rooms") && room.isElementVideoRoom();
|
||||
|
||||
let inviteOption: JSX.Element;
|
||||
if (room.canInvite(cli.getUserId()) && !isDm) {
|
||||
|
@ -233,11 +234,27 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
|||
/>;
|
||||
}
|
||||
|
||||
const pinningEnabled = useSettingValue("feature_pinning");
|
||||
let filesOption: JSX.Element;
|
||||
if (!isVideoRoom) {
|
||||
filesOption = <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
ensureViewingRoom(ev);
|
||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, false);
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Files")}
|
||||
iconClassName="mx_RoomTile_iconFiles"
|
||||
/>;
|
||||
}
|
||||
|
||||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
||||
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
|
||||
|
||||
let pinsOption: JSX.Element;
|
||||
if (pinningEnabled) {
|
||||
if (pinningEnabled && !isVideoRoom) {
|
||||
pinsOption = <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
|
@ -256,6 +273,37 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
|||
</IconizedContextMenuOption>;
|
||||
}
|
||||
|
||||
let widgetsOption: JSX.Element;
|
||||
if (!isVideoRoom) {
|
||||
widgetsOption = <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
ensureViewingRoom(ev);
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }, false);
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Widgets")}
|
||||
iconClassName="mx_RoomTile_iconWidgets"
|
||||
/>;
|
||||
}
|
||||
|
||||
let exportChatOption: JSX.Element;
|
||||
if (!isVideoRoom) {
|
||||
exportChatOption = <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Export room dialog', '', ExportDialog, { room });
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Export chat")}
|
||||
iconClassName="mx_RoomTile_iconExport"
|
||||
/>;
|
||||
}
|
||||
|
||||
const onTagRoom = (ev: ButtonEvent, tagId: TagID) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -295,35 +343,9 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
|||
{ notificationOption }
|
||||
{ favouriteOption }
|
||||
{ peopleOption }
|
||||
|
||||
<IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
ensureViewingRoom(ev);
|
||||
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, false);
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Files")}
|
||||
iconClassName="mx_RoomTile_iconFiles"
|
||||
/>
|
||||
|
||||
{ filesOption }
|
||||
{ pinsOption }
|
||||
|
||||
<IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
ensureViewingRoom(ev);
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }, false);
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Widgets")}
|
||||
iconClassName="mx_RoomTile_iconWidgets"
|
||||
/>
|
||||
|
||||
{ widgetsOption }
|
||||
{ lowPriorityOption }
|
||||
{ copyLinkOption }
|
||||
|
||||
|
@ -343,17 +365,7 @@ const RoomContextMenu = ({ room, onFinished, ...props }: IProps) => {
|
|||
iconClassName="mx_RoomTile_iconSettings"
|
||||
/>
|
||||
|
||||
<IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Export room dialog', '', ExportDialog, { room });
|
||||
onFinished();
|
||||
}}
|
||||
label={_t("Export chat")}
|
||||
iconClassName="mx_RoomTile_iconExport"
|
||||
/>
|
||||
{ exportChatOption }
|
||||
|
||||
{ SettingsStore.getValue("developerMode") && <IconizedContextMenuOption
|
||||
onClick={(ev: ButtonEvent) => {
|
||||
|
|
|
@ -35,7 +35,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
|
|||
const info = SettingsStore.getBetaInfo(featureId);
|
||||
|
||||
return <GenericFeatureFeedbackDialog
|
||||
title={_t("%(featureName)s beta feedback", { featureName: info.title })}
|
||||
title={_t("%(featureName)s Beta feedback", { featureName: info.title })}
|
||||
subheading={_t(info.feedbackSubheading)}
|
||||
onFinished={onFinished}
|
||||
rageshakeLabel={info.feedbackLabel}
|
||||
|
|
|
@ -21,14 +21,11 @@ import { RoomType } from "matrix-js-sdk/src/@types/event";
|
|||
import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials";
|
||||
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import withValidation, { IFieldState } from '../elements/Validation';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import { IOpts } from "../../../createRoom";
|
||||
import Heading from "../typography/Heading";
|
||||
import { IOpts, privateShouldBeEncrypted } from "../../../createRoom";
|
||||
import Field from "../elements/Field";
|
||||
import StyledRadioGroup from "../elements/StyledRadioGroup";
|
||||
import RoomAliasField from "../elements/RoomAliasField";
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import DialogButtons from "../elements/DialogButtons";
|
||||
|
@ -40,6 +37,7 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||
|
||||
interface IProps {
|
||||
type?: RoomType;
|
||||
defaultPublic?: boolean;
|
||||
defaultName?: string;
|
||||
parentSpace?: Room;
|
||||
|
@ -48,7 +46,6 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
type?: RoomType;
|
||||
joinRule: JoinRule;
|
||||
isPublic: boolean;
|
||||
isEncrypted: boolean;
|
||||
|
@ -79,7 +76,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
this.state = {
|
||||
type: null,
|
||||
isPublic: this.props.defaultPublic || false,
|
||||
isEncrypted: this.props.defaultEncrypted ?? privateShouldBeEncrypted(),
|
||||
joinRule,
|
||||
|
@ -99,7 +95,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
private roomCreateOptions() {
|
||||
const opts: IOpts = {};
|
||||
const createOpts: IOpts["createOpts"] = opts.createOpts = {};
|
||||
opts.roomType = this.state.type;
|
||||
opts.roomType = this.props.type;
|
||||
createOpts.name = this.state.name;
|
||||
|
||||
if (this.state.joinRule === JoinRule.Public) {
|
||||
|
@ -179,10 +175,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
private onTypeChange = (type: RoomType | "text") => {
|
||||
this.setState({ type: type === "text" ? null : type });
|
||||
};
|
||||
|
||||
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ name: ev.target.value });
|
||||
};
|
||||
|
@ -228,6 +220,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
render() {
|
||||
const isVideoRoom = this.props.type === RoomType.ElementVideo;
|
||||
|
||||
let aliasField;
|
||||
if (this.state.joinRule === JoinRule.Public) {
|
||||
const domain = MatrixClientPeg.get().getDomain();
|
||||
|
@ -318,8 +312,12 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
let title = _t("Create a room");
|
||||
if (!this.props.parentSpace) {
|
||||
let title;
|
||||
if (isVideoRoom) {
|
||||
title = _t("Create a video room");
|
||||
} else if (this.props.parentSpace) {
|
||||
title = _t("Create a room");
|
||||
} else {
|
||||
title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room');
|
||||
}
|
||||
|
||||
|
@ -332,20 +330,6 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
>
|
||||
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_content">
|
||||
{ SettingsStore.getValue("feature_voice_rooms") ? <>
|
||||
<Heading size="h3">{ _t("Room type") }</Heading>
|
||||
<StyledRadioGroup
|
||||
name="type"
|
||||
value={this.state.type ?? "text"}
|
||||
onChange={this.onTypeChange}
|
||||
definitions={[
|
||||
{ value: "text", label: _t("Text room") },
|
||||
{ value: RoomType.UnstableCall, label: _t("Voice & video room") },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Heading size="h3">{ _t("Room details") }</Heading>
|
||||
</> : null }
|
||||
<Field
|
||||
ref={this.nameField}
|
||||
label={_t('Name')}
|
||||
|
@ -389,7 +373,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||
</details>
|
||||
</div>
|
||||
</form>
|
||||
<DialogButtons primaryButton={_t('Create Room')}
|
||||
<DialogButtons primaryButton={isVideoRoom ? _t('Create video room') : _t('Create room')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { HTMLAttributes, ReactNode, useContext } from "react";
|
||||
import React, { FC, HTMLAttributes, ReactNode, useContext } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { sortBy } from "lodash";
|
||||
|
@ -26,48 +26,17 @@ import TextWithTooltip from "../elements/TextWithTooltip";
|
|||
import { useRoomMembers } from "../../../hooks/useRoomMembers";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
faces: ReactNode[];
|
||||
overflow: boolean;
|
||||
tooltip?: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const FacePile = ({ faces, overflow, tooltip, children, ...props }: IProps) => {
|
||||
const pileContents = <>
|
||||
{ overflow ? <span className="mx_FacePile_more" /> : null }
|
||||
{ faces }
|
||||
</>;
|
||||
|
||||
return <div {...props} className="mx_FacePile">
|
||||
{ tooltip ? (
|
||||
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
|
||||
{ pileContents }
|
||||
</TextWithTooltip>
|
||||
) : (
|
||||
<div className="mx_FacePile_faces">
|
||||
{ pileContents }
|
||||
</div>
|
||||
) }
|
||||
{ children }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default FacePile;
|
||||
|
||||
const DEFAULT_NUM_FACES = 5;
|
||||
|
||||
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
|
||||
|
||||
interface IRoomProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
room: Room;
|
||||
onlyKnownUsers?: boolean;
|
||||
numShown?: number;
|
||||
}
|
||||
|
||||
export const RoomFacePile = (
|
||||
{ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IRoomProps,
|
||||
) => {
|
||||
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
|
||||
|
||||
const FacePile: FC<IProps> = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const isJoined = room.getMyMembership() === "join";
|
||||
let members = useRoomMembers(room);
|
||||
|
@ -89,8 +58,6 @@ export const RoomFacePile = (
|
|||
// We reverse the order of the shown faces in CSS to simplify their visual overlap,
|
||||
// reverse members in tooltip order to make the order between the two match up.
|
||||
const commaSeparatedMembers = shownMembers.map(m => m.rawDisplayName).reverse().join(", ");
|
||||
const faces = shownMembers.map(m =>
|
||||
<MemberAvatar key={m.userId} member={m} width={28} height={28} />);
|
||||
|
||||
let tooltip: ReactNode;
|
||||
if (props.onClick) {
|
||||
|
@ -123,9 +90,16 @@ export const RoomFacePile = (
|
|||
}
|
||||
}
|
||||
|
||||
return <FacePile faces={faces} overflow={members.length > numShown} tooltip={tooltip}>
|
||||
return <div {...props} className="mx_FacePile">
|
||||
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip} tooltipProps={{ yOffset: 32 }}>
|
||||
{ members.length > numShown ? <span className="mx_FacePile_face mx_FacePile_more" /> : null }
|
||||
{ shownMembers.map(m =>
|
||||
<MemberAvatar key={m.userId} member={m} width={28} height={28} className="mx_FacePile_face" />) }
|
||||
</TextWithTooltip>
|
||||
{ onlyKnownUsers && <span className="mx_FacePile_summary">
|
||||
{ _t("%(count)s people you know have already joined", { count: members.length }) }
|
||||
</span> }
|
||||
</FacePile>;
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default FacePile;
|
||||
|
|
|
@ -77,7 +77,7 @@ const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => {
|
|||
className="mx_JumpToDatePicker_form"
|
||||
onSubmit={onJumpToDateSubmit}
|
||||
>
|
||||
<span className="mx_JumpToDatePicker_label">Jump to date</span>
|
||||
<span className="mx_JumpToDatePicker_label">{ _t("Jump to date") }</span>
|
||||
<Field
|
||||
componentClass={NativeOnChangeInput}
|
||||
type="date"
|
||||
|
|
|
@ -43,6 +43,8 @@ import { showThread } from "../../../dispatcher/dispatch-actions/threads";
|
|||
import { shouldDisplayReply } from '../../../utils/Reply';
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { UserTab } from '../dialogs/UserSettingsDialog';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
|
||||
interface IOptionsButtonProps {
|
||||
mxEvent: MatrixEvent;
|
||||
|
@ -221,7 +223,18 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
};
|
||||
|
||||
private onThreadClick = (isCard: boolean): void => {
|
||||
showThread({ rootEvent: this.props.mxEvent, push: isCard });
|
||||
if (localStorage.getItem("mx_seen_feature_thread") === null) {
|
||||
localStorage.setItem("mx_seen_feature_thread", "true");
|
||||
}
|
||||
|
||||
if (!SettingsStore.getValue("feature_thread")) {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
} else {
|
||||
showThread({ rootEvent: this.props.mxEvent, push: isCard });
|
||||
}
|
||||
};
|
||||
|
||||
private onEditClick = (): void => {
|
||||
|
@ -233,14 +246,13 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
];
|
||||
|
||||
private get showReplyInThreadAction(): boolean {
|
||||
const isThreadEnabled = SettingsStore.getValue("feature_thread");
|
||||
const inNotThreadTimeline = this.context.timelineRenderingType !== TimelineRenderingType.Thread;
|
||||
|
||||
const isAllowedMessageType = !this.forbiddenThreadHeadMsgType.includes(
|
||||
this.props.mxEvent.getContent().msgtype as MsgType,
|
||||
);
|
||||
|
||||
return isThreadEnabled && inNotThreadTimeline && isAllowedMessageType;
|
||||
return inNotThreadTimeline && isAllowedMessageType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -296,21 +308,42 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
key="cancel"
|
||||
/>;
|
||||
|
||||
const hasARelation = !!this.props.mxEvent?.getRelation()?.rel_type;
|
||||
|
||||
const relationType = this.props.mxEvent?.getRelation()?.rel_type;
|
||||
const hasARelation = !!relationType && relationType !== RelationType.Thread;
|
||||
const firstTimeSeeingThreads = localStorage.getItem("mx_seen_feature_thread") === null &&
|
||||
!SettingsStore.getValue("feature_thread");
|
||||
const threadTooltipButton = <CardContext.Consumer key="thread">
|
||||
{ context =>
|
||||
<RovingAccessibleTooltipButton
|
||||
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
|
||||
|
||||
disabled={hasARelation}
|
||||
tooltip={<>
|
||||
<div className="mx_Tooltip_title">
|
||||
{ !hasARelation
|
||||
? _t("Reply in thread")
|
||||
: _t("Can't create a thread from an event with an existing relation") }
|
||||
</div>
|
||||
{ !hasARelation && (
|
||||
<div className="mx_Tooltip_sub">
|
||||
{ SettingsStore.getValue("feature_thread")
|
||||
? _t("Beta feature")
|
||||
: _t("Beta feature. Click to learn more.")
|
||||
}
|
||||
</div>
|
||||
) }
|
||||
</>}
|
||||
|
||||
title={!hasARelation
|
||||
? _t("Reply in thread")
|
||||
: _t("Can't create a thread from an event with an existing relation")
|
||||
}
|
||||
: _t("Can't create a thread from an event with an existing relation")}
|
||||
|
||||
onClick={this.onThreadClick.bind(null, context.isCard)}
|
||||
/>
|
||||
>
|
||||
{ firstTimeSeeingThreads && (
|
||||
<div className="mx_Indicator" />
|
||||
) }
|
||||
</RovingAccessibleTooltipButton>
|
||||
}
|
||||
</CardContext.Consumer>;
|
||||
|
||||
|
@ -385,14 +418,14 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
|
|||
'mx_MessageActionBar_expandMessageButton': !this.props.isQuoteExpanded,
|
||||
'mx_MessageActionBar_collapseMessageButton': this.props.isQuoteExpanded,
|
||||
});
|
||||
const tooltip = <div>
|
||||
const tooltip = <>
|
||||
<div className="mx_Tooltip_title">
|
||||
{ this.props.isQuoteExpanded ? _t("Collapse quotes") : _t("Expand quotes") }
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{ _t(ALTERNATE_KEY_NAME[Key.SHIFT]) + " + " + _t("Click") }
|
||||
</div>
|
||||
</div>;
|
||||
</>;
|
||||
toolbarOpts.push(<RovingAccessibleTooltipButton
|
||||
className={expandClassName}
|
||||
title={this.props.isQuoteExpanded ? _t("Collapse quotes") : _t("Expand quotes")}
|
||||
|
|
|
@ -605,9 +605,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
if (this.props.highlightLink) {
|
||||
body = <a href={this.props.highlightLink}>{ body }</a>;
|
||||
} else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") {
|
||||
body = <AccessibleButton kind="link_inline"
|
||||
onClick={this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"])}
|
||||
>{ body }</AccessibleButton>;
|
||||
body = (
|
||||
<AccessibleButton
|
||||
kind="link_inline"
|
||||
onClick={this.onStarterLinkClick.bind(this, content.data["org.matrix.neb.starter_link"])}
|
||||
>
|
||||
{ body }
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let widgets;
|
||||
|
@ -649,9 +654,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
|||
);
|
||||
}
|
||||
return (
|
||||
<div className="mx_MTextBody mx_EventTile_content"
|
||||
onClick={this.onBodyLinkClick}
|
||||
>
|
||||
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
|
||||
{ body }
|
||||
{ widgets }
|
||||
</div>
|
||||
|
|
|
@ -28,6 +28,7 @@ import { ButtonEvent } from "../elements/AccessibleButton";
|
|||
interface IProps {
|
||||
// Whether this button is highlighted
|
||||
isHighlighted: boolean;
|
||||
isUnread?: boolean;
|
||||
// click handler
|
||||
onClick: (ev: ButtonEvent) => void;
|
||||
// The parameters to track the click event
|
||||
|
@ -48,11 +49,12 @@ export default class HeaderButton extends React.Component<IProps> {
|
|||
|
||||
public render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { isHighlighted, onClick, analytics, name, title, ...props } = this.props;
|
||||
const { isHighlighted, isUnread = false, onClick, analytics, name, title, ...props } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
mx_RightPanel_headerButton: true,
|
||||
mx_RightPanel_headerButton_highlight: isHighlighted,
|
||||
mx_RightPanel_headerButton_unread: isUnread,
|
||||
[`mx_RightPanel_${name}`]: true,
|
||||
});
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ const UnreadIndicator = ({ color }: IUnreadIndicatorProps) => {
|
|||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_Indicator": true,
|
||||
"mx_RightPanel_headerButton_unreadIndicator": true,
|
||||
"mx_Indicator_bold": color === NotificationColor.Bold,
|
||||
"mx_Indicator_gray": color === NotificationColor.Grey,
|
||||
|
@ -92,6 +93,7 @@ const PinnedMessagesHeaderButton = ({ room, isHighlighted, onClick }: IHeaderBut
|
|||
name="pinnedMessagesButton"
|
||||
title={_t("Pinned messages")}
|
||||
isHighlighted={isHighlighted}
|
||||
isUnread={!!unreadIndicator}
|
||||
onClick={onClick}
|
||||
analytics={["Right Panel", "Pinned Messages Button", "click"]}
|
||||
>
|
||||
|
@ -241,6 +243,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
title={_t("Threads")}
|
||||
onClick={this.onThreadsPanelClicked}
|
||||
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
|
||||
isUnread={this.threadNotificationState.color > 0}
|
||||
analytics={['Right Panel', 'Threads List Button', 'click']}>
|
||||
<UnreadIndicator color={this.threadNotificationState.color} />
|
||||
</HeaderButton>
|
||||
|
|
|
@ -42,7 +42,7 @@ import { UIComponent, UIFeature } from "../../../settings/UIFeature";
|
|||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import WidgetContextMenu from "../context_menus/WidgetContextMenu";
|
||||
import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import { usePinnedEvents } from "./PinnedMessagesCard";
|
||||
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
@ -269,6 +269,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
const isRoomEncrypted = useIsEncrypted(cli, room);
|
||||
const roomContext = useContext(RoomContext);
|
||||
const e2eStatus = roomContext.e2eStatus;
|
||||
const isVideoRoom = useFeatureEnabled("feature_video_rooms") && room.isElementVideoRoom();
|
||||
|
||||
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
|
||||
const header = <React.Fragment>
|
||||
|
@ -297,7 +298,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
</React.Fragment>;
|
||||
|
||||
const memberCount = useRoomMemberCount(room);
|
||||
const pinningEnabled = useSettingValue("feature_pinning");
|
||||
const pinningEnabled = useFeatureEnabled("feature_pinning");
|
||||
const pinCount = usePinnedEvents(pinningEnabled && room)?.length;
|
||||
|
||||
return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
|
||||
|
@ -308,18 +309,19 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
{ memberCount }
|
||||
</span>
|
||||
</Button>
|
||||
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{ !isVideoRoom && <Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
|
||||
{ _t("Files") }
|
||||
</Button>
|
||||
{ pinningEnabled && <Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
||||
{ _t("Pinned") }
|
||||
{ pinCount > 0 && <span className="mx_BaseCard_Button_sublabel">
|
||||
{ pinCount }
|
||||
</span> }
|
||||
</Button> }
|
||||
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||
{ pinningEnabled && !isVideoRoom &&
|
||||
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
|
||||
{ _t("Pinned") }
|
||||
{ pinCount > 0 && <span className="mx_BaseCard_Button_sublabel">
|
||||
{ pinCount }
|
||||
</span> }
|
||||
</Button> }
|
||||
{ !isVideoRoom && <Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
|
||||
{ _t("Export chat") }
|
||||
</Button>
|
||||
</Button> }
|
||||
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
|
||||
{ _t("Share room") }
|
||||
</Button>
|
||||
|
@ -330,6 +332,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
|
|||
|
||||
{
|
||||
SettingsStore.getValue(UIFeature.Widgets)
|
||||
&& !isVideoRoom
|
||||
&& shouldShowComponent(UIComponent.AddIntegrations)
|
||||
&& <AppsSection room={room} />
|
||||
}
|
||||
|
|
|
@ -491,7 +491,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
* when we are at the sync stage
|
||||
*/
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const thread = room?.threads.get(this.props.mxEvent.getId());
|
||||
const thread = room?.threads?.get(this.props.mxEvent.getId());
|
||||
|
||||
return thread || null;
|
||||
}
|
||||
|
@ -510,12 +510,22 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private renderThreadInfo(): React.ReactNode {
|
||||
if (this.state.thread?.id === this.props.mxEvent.getId()) {
|
||||
return <ThreadSummary mxEvent={this.props.mxEvent} thread={this.state.thread} />;
|
||||
}
|
||||
|
||||
if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) {
|
||||
if (this.props.highlightLink) {
|
||||
return (
|
||||
<a className="mx_ThreadSummaryIcon" href={this.props.highlightLink}>
|
||||
{ _t("From a thread") }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="mx_ThreadSummaryIcon">{ _t("From a thread") }</p>
|
||||
);
|
||||
} else if (this.state.thread?.id === this.props.mxEvent.getId()) {
|
||||
return <ThreadSummary mxEvent={this.props.mxEvent} thread={this.state.thread} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -978,6 +988,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
let isContinuation = this.props.continuation;
|
||||
if (this.context.timelineRenderingType !== TimelineRenderingType.Room &&
|
||||
this.context.timelineRenderingType !== TimelineRenderingType.Search &&
|
||||
this.context.timelineRenderingType !== TimelineRenderingType.Thread &&
|
||||
this.props.layout !== Layout.Bubble
|
||||
) {
|
||||
isContinuation = false;
|
||||
|
@ -1024,16 +1035,17 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
? undefined
|
||||
: this.props.mxEvent.getId();
|
||||
|
||||
let avatar;
|
||||
let sender;
|
||||
let avatarSize;
|
||||
let needsSenderProfile;
|
||||
let avatar: JSX.Element;
|
||||
let sender: JSX.Element;
|
||||
let avatarSize: number;
|
||||
let needsSenderProfile: boolean;
|
||||
|
||||
if (this.context.timelineRenderingType === TimelineRenderingType.Notification ||
|
||||
this.context.timelineRenderingType === TimelineRenderingType.ThreadsList
|
||||
) {
|
||||
if (this.context.timelineRenderingType === TimelineRenderingType.Notification) {
|
||||
avatarSize = 24;
|
||||
needsSenderProfile = true;
|
||||
} else if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) {
|
||||
avatarSize = 36;
|
||||
needsSenderProfile = true;
|
||||
} else if (eventType === EventType.RoomCreate || isBubbleMessage) {
|
||||
avatarSize = 0;
|
||||
needsSenderProfile = false;
|
||||
|
@ -1281,9 +1293,7 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
</div>,
|
||||
<div className="mx_EventTile_senderDetails" key="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ sender }
|
||||
</a>
|
||||
{ sender }
|
||||
</div>,
|
||||
<div className={lineClasses} key="mx_EventTile_line">
|
||||
{ replyChain }
|
||||
|
@ -1301,7 +1311,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
|||
permalinkCreator: this.props.permalinkCreator,
|
||||
}) }
|
||||
{ actionBar }
|
||||
{ timestamp }
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ timestamp }
|
||||
</a>
|
||||
</div>,
|
||||
reactionsRow,
|
||||
]);
|
||||
|
|
|
@ -204,6 +204,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
const buttons: JSX.Element[] = [];
|
||||
|
||||
if (this.props.inRoom &&
|
||||
this.props.onCallPlaced &&
|
||||
!this.context.tombstone &&
|
||||
SettingsStore.getValue("showCallButtonsInComposer")
|
||||
) {
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import React, { ComponentType, createRef, ReactComponentElement, RefObject } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
|
@ -221,8 +222,8 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
|||
showCreateRoom
|
||||
? (<>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconCreateNewRoom"
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -234,6 +235,19 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
|||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/>
|
||||
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
showCreateNewRoom(activeSpace, RoomType.ElementVideo);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/> }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconAddExistingRoom"
|
||||
|
@ -253,17 +267,32 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
|||
</IconizedContextMenuOptionList>;
|
||||
} else if (menuDisplayed) {
|
||||
contextMenuContent = <IconizedContextMenuOptionList first>
|
||||
{ showCreateRoom && <IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconCreateNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
||||
}}
|
||||
/> }
|
||||
{ showCreateRoom && <>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
||||
}}
|
||||
/>
|
||||
{ SettingsStore.getValue("feature_video_rooms") && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_create_room",
|
||||
type: RoomType.ElementVideo,
|
||||
});
|
||||
}}
|
||||
/> }
|
||||
</> }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
|
|
|
@ -16,11 +16,12 @@ limitations under the License.
|
|||
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useEventEmitterState, useTypedEventEmitter, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
||||
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import SpaceContextMenu from "../context_menus/SpaceContextMenu";
|
||||
|
@ -127,6 +128,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => {
|
||||
return SpaceStore.instance.allRoomsInHome;
|
||||
});
|
||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
const pendingActions = usePendingActions();
|
||||
|
||||
const filterCondition = RoomListStore.instance.getFirstNameFilterCondition();
|
||||
|
@ -195,19 +197,31 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
/>;
|
||||
}
|
||||
|
||||
let createNewRoomOption: JSX.Element;
|
||||
let newRoomOptions: JSX.Element;
|
||||
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
|
||||
createNewRoomOption = <IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconCreateRoom"
|
||||
label={_t("Create new room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(activeSpace);
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>;
|
||||
newRoomOptions = <>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconNewRoom"
|
||||
label={_t("New room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(activeSpace);
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||
label={_t("New video room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(activeSpace, RoomType.ElementVideo);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/> }
|
||||
</>;
|
||||
}
|
||||
|
||||
contextMenu = <IconizedContextMenu
|
||||
|
@ -217,7 +231,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ inviteOption }
|
||||
{ createNewRoomOption }
|
||||
{ newRoomOptions }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomListHeader_iconExplore"
|
||||
|
@ -262,12 +276,11 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
} else if (plusMenuDisplayed) {
|
||||
let startChatOpt: JSX.Element;
|
||||
let createRoomOpt: JSX.Element;
|
||||
let newRoomOpts: JSX.Element;
|
||||
let joinRoomOpt: JSX.Element;
|
||||
|
||||
if (canCreateRooms) {
|
||||
startChatOpt = (
|
||||
newRoomOpts = <>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomListHeader_iconStartChat"
|
||||
|
@ -278,11 +291,9 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
createRoomOpt = (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomListHeader_iconCreateRoom"
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -291,7 +302,20 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
{ videoRoomsEnabled && <IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_create_room",
|
||||
type: RoomType.ElementVideo,
|
||||
});
|
||||
closePlusMenu();
|
||||
}}
|
||||
/> }
|
||||
</>;
|
||||
}
|
||||
if (canExploreRooms) {
|
||||
joinRoomOpt = (
|
||||
|
@ -314,8 +338,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ startChatOpt }
|
||||
{ createRoomOpt }
|
||||
{ newRoomOpts }
|
||||
{ joinRoomOpt }
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
|
|
|
@ -32,10 +32,7 @@ import { _t } from "../../../languageHandler";
|
|||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import FacePile from "../elements/FacePile";
|
||||
import { RoomNotifState } from "../../../RoomNotifs";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
|
@ -54,17 +51,16 @@ import IconizedContextMenu, {
|
|||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import VoiceChannelStore, { VoiceChannelEvent, IJitsiParticipant } from "../../../stores/VoiceChannelStore";
|
||||
import { getConnectedMembers } from "../../../utils/VoiceChannelUtils";
|
||||
import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../../../stores/VideoChannelStore";
|
||||
import { getConnectedMembers } from "../../../utils/VideoChannelUtils";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { RoomViewStore } from "../../../stores/RoomViewStore";
|
||||
|
||||
enum VoiceConnectionState {
|
||||
enum VideoStatus {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
}
|
||||
|
||||
|
@ -82,10 +78,10 @@ interface IState {
|
|||
notificationsMenuPosition: PartialDOMRect;
|
||||
generalMenuPosition: PartialDOMRect;
|
||||
messagePreview?: string;
|
||||
voiceConnectionState: VoiceConnectionState;
|
||||
// Active voice channel members, according to room state
|
||||
voiceMembers: RoomMember[];
|
||||
// Active voice channel members, according to Jitsi
|
||||
videoStatus: VideoStatus;
|
||||
// Active video channel members, according to room state
|
||||
videoMembers: RoomMember[];
|
||||
// Active video channel members, according to Jitsi
|
||||
jitsiParticipants: IJitsiParticipant[];
|
||||
}
|
||||
|
||||
|
@ -104,27 +100,28 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
private roomTileRef = createRef<HTMLDivElement>();
|
||||
private notificationState: NotificationState;
|
||||
private roomProps: RoomEchoChamber;
|
||||
private isVoiceRoom: boolean;
|
||||
private isVideoRoom: boolean;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const videoConnected = VideoChannelStore.instance.roomId === this.props.room.roomId;
|
||||
|
||||
this.state = {
|
||||
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: "",
|
||||
voiceConnectionState: VoiceChannelStore.instance.roomId === this.props.room.roomId ?
|
||||
VoiceConnectionState.Connected : VoiceConnectionState.Disconnected,
|
||||
voiceMembers: [],
|
||||
jitsiParticipants: [],
|
||||
videoStatus: videoConnected ? VideoStatus.Connected : VideoStatus.Disconnected,
|
||||
videoMembers: getConnectedMembers(this.props.room.currentState),
|
||||
jitsiParticipants: videoConnected ? VideoChannelStore.instance.participants : [],
|
||||
};
|
||||
this.generatePreview();
|
||||
|
||||
this.notificationState = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||
this.roomProps = EchoChamber.forRoom(this.props.room);
|
||||
this.isVoiceRoom = SettingsStore.getValue("feature_voice_rooms") && this.props.room.isCallRoom();
|
||||
this.isVideoRoom = SettingsStore.getValue("feature_video_rooms") && this.props.room.isElementVideoRoom();
|
||||
}
|
||||
|
||||
private onRoomNameUpdate = (room: Room) => {
|
||||
|
@ -163,8 +160,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
MessagePreviewStore.getPreviewChangedEventName(this.props.room),
|
||||
this.onRoomPreviewChanged,
|
||||
);
|
||||
prevProps.room?.currentState?.off(RoomStateEvent.Events, this.updateVoiceMembers);
|
||||
this.props.room?.currentState?.on(RoomStateEvent.Events, this.updateVoiceMembers);
|
||||
prevProps.room?.currentState?.off(RoomStateEvent.Events, this.updateVideoMembers);
|
||||
this.props.room?.currentState?.on(RoomStateEvent.Events, this.updateVideoMembers);
|
||||
this.updateVideoStatus();
|
||||
prevProps.room?.off(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
this.props.room?.on(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
}
|
||||
|
@ -175,7 +173,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
if (this.state.selected) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
this.updateVoiceMembers();
|
||||
|
||||
RoomViewStore.instance.addRoomListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
|
@ -186,7 +183,13 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.notificationState.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
this.props.room.on(RoomEvent.Name, this.onRoomNameUpdate);
|
||||
this.props.room.currentState.on(RoomStateEvent.Events, this.updateVoiceMembers);
|
||||
this.props.room.currentState.on(RoomStateEvent.Events, this.updateVideoMembers);
|
||||
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Connect, this.updateVideoStatus);
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Disconnect, this.updateVideoStatus);
|
||||
if (VideoChannelStore.instance.roomId === this.props.room.roomId) {
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
@ -200,6 +203,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
this.notificationState.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.roomProps.off(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
|
||||
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Connect, this.updateVideoStatus);
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Disconnect, this.updateVideoStatus);
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
|
@ -250,11 +256,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
metricsTrigger: "RoomList",
|
||||
metricsViaKeyboard: ev.type !== "click",
|
||||
});
|
||||
|
||||
// Connect to the voice channel if this is a voice room
|
||||
if (this.isVoiceRoom && this.state.voiceConnectionState === VoiceConnectionState.Disconnected) {
|
||||
await this.connectVoice();
|
||||
}
|
||||
};
|
||||
|
||||
private onActiveRoomUpdate = (isActive: boolean) => {
|
||||
|
@ -579,87 +580,24 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
);
|
||||
}
|
||||
|
||||
private updateVoiceMembers = () => {
|
||||
this.setState({ voiceMembers: getConnectedMembers(this.props.room.currentState) });
|
||||
private updateVideoMembers = () => {
|
||||
this.setState({ videoMembers: getConnectedMembers(this.props.room.currentState) });
|
||||
};
|
||||
|
||||
private updateVideoStatus = () => {
|
||||
if (VideoChannelStore.instance.roomId === this.props.room?.roomId) {
|
||||
this.setState({ videoStatus: VideoStatus.Connected });
|
||||
VideoChannelStore.instance.on(VideoChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
} else {
|
||||
this.setState({ videoStatus: VideoStatus.Disconnected });
|
||||
VideoChannelStore.instance.off(VideoChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
}
|
||||
};
|
||||
|
||||
private updateJitsiParticipants = (participants: IJitsiParticipant[]) => {
|
||||
this.setState({ jitsiParticipants: participants });
|
||||
};
|
||||
|
||||
private renderVoiceChannel(): React.ReactElement | null {
|
||||
let faces;
|
||||
if (this.state.voiceConnectionState === VoiceConnectionState.Connected) {
|
||||
faces = this.state.jitsiParticipants.map(p =>
|
||||
<BaseAvatar
|
||||
key={p.participantId}
|
||||
name={p.displayName ?? p.formattedDisplayName}
|
||||
idName={p.participantId}
|
||||
// This comes directly from Jitsi, so we shouldn't apply custom media routing to it
|
||||
url={p.avatarURL}
|
||||
width={24}
|
||||
height={24}
|
||||
/>,
|
||||
);
|
||||
} else if (this.state.voiceMembers.length) {
|
||||
faces = this.state.voiceMembers.map(m =>
|
||||
<MemberAvatar
|
||||
key={m.userId}
|
||||
member={m}
|
||||
width={24}
|
||||
height={24}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: The below "join" button will eventually show up on text rooms
|
||||
// with an active voice channel, but that isn't implemented yet
|
||||
return <div className="mx_RoomTile_voiceChannel">
|
||||
<FacePile faces={faces} overflow={false} />
|
||||
{ this.isVoiceRoom ? null : (
|
||||
<AccessibleButton
|
||||
kind="link"
|
||||
className="mx_RoomTile_connectVoiceButton"
|
||||
onClick={this.connectVoice.bind(this)}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
) }
|
||||
</div>;
|
||||
}
|
||||
|
||||
private async connectVoice() {
|
||||
this.setState({ voiceConnectionState: VoiceConnectionState.Connecting });
|
||||
// TODO: Actually wait for the widget to be ready, instead of guessing.
|
||||
// This hack is only in place until we find out for sure whether design
|
||||
// wants the room view to open when connecting voice, or if this should
|
||||
// somehow connect in the background. Until then, it's not worth the
|
||||
// effort to solve this properly.
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const waitForConnect = VoiceChannelStore.instance.connect(this.props.room.roomId);
|
||||
// Participant data comes down the event channel quickly, so prepare in advance
|
||||
VoiceChannelStore.instance.on(VoiceChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
try {
|
||||
await waitForConnect;
|
||||
this.setState({ voiceConnectionState: VoiceConnectionState.Connected });
|
||||
|
||||
VoiceChannelStore.instance.once(VoiceChannelEvent.Disconnect, () => {
|
||||
this.setState({
|
||||
voiceConnectionState: VoiceConnectionState.Disconnected,
|
||||
jitsiParticipants: [],
|
||||
}),
|
||||
VoiceChannelStore.instance.off(VoiceChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
});
|
||||
} catch (e) {
|
||||
// If it failed, clean up our advance preparations
|
||||
logger.error("Failed to connect voice", e);
|
||||
VoiceChannelStore.instance.off(VoiceChannelEvent.Participants, this.updateJitsiParticipants);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
|
@ -687,34 +625,44 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
let subtitle;
|
||||
if (this.isVoiceRoom) {
|
||||
switch (this.state.voiceConnectionState) {
|
||||
case VoiceConnectionState.Disconnected:
|
||||
subtitle = (
|
||||
<div className="mx_RoomTile_subtitle mx_RoomTile_voiceIndicator">
|
||||
{ _t("Voice room") }
|
||||
</div>
|
||||
);
|
||||
if (this.isVideoRoom) {
|
||||
let videoText: string;
|
||||
let videoActive: boolean;
|
||||
let participantCount: number;
|
||||
|
||||
switch (this.state.videoStatus) {
|
||||
case VideoStatus.Disconnected:
|
||||
videoText = _t("Video");
|
||||
videoActive = false;
|
||||
participantCount = this.state.videoMembers.length;
|
||||
break;
|
||||
case VoiceConnectionState.Connecting:
|
||||
subtitle = (
|
||||
<div className="mx_RoomTile_subtitle mx_RoomTile_voiceIndicator">
|
||||
{ _t("Connecting...") }
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case VoiceConnectionState.Connected:
|
||||
subtitle = (
|
||||
<div
|
||||
className={
|
||||
"mx_RoomTile_subtitle mx_RoomTile_voiceIndicator " +
|
||||
"mx_RoomTile_voiceIndicator_active"
|
||||
}
|
||||
>
|
||||
{ _t("Connected") }
|
||||
</div>
|
||||
);
|
||||
case VideoStatus.Connected:
|
||||
videoText = _t("Connected");
|
||||
videoActive = true;
|
||||
participantCount = this.state.jitsiParticipants.length;
|
||||
}
|
||||
|
||||
subtitle = (
|
||||
<div className="mx_RoomTile_subtitle">
|
||||
<span
|
||||
className={classNames({
|
||||
"mx_RoomTile_videoIndicator": true,
|
||||
"mx_RoomTile_videoIndicator_active": videoActive,
|
||||
})}
|
||||
>
|
||||
{ videoText }
|
||||
</span>
|
||||
{ participantCount ? <>
|
||||
{ " · " }
|
||||
<span
|
||||
className="mx_RoomTile_videoParticipants"
|
||||
aria-label={_t("%(count)s participants", { count: participantCount })}
|
||||
>
|
||||
{ participantCount }
|
||||
</span>
|
||||
</> : null }
|
||||
</div>
|
||||
);
|
||||
} else if (this.showMessagePreview && this.state.messagePreview) {
|
||||
subtitle = (
|
||||
<div
|
||||
|
@ -795,15 +743,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
displayBadge={this.props.isMinimized}
|
||||
tooltipProps={{ tabIndex: isActive ? 0 : -1 }}
|
||||
/>
|
||||
<div className="mx_RoomTile_details">
|
||||
<div className="mx_RoomTile_primaryDetails">
|
||||
{ titleContainer }
|
||||
{ badge }
|
||||
{ this.renderGeneralMenu() }
|
||||
{ this.renderNotificationsMenu(isActive) }
|
||||
</div>
|
||||
{ this.renderVoiceChannel() }
|
||||
</div>
|
||||
{ titleContainer }
|
||||
{ badge }
|
||||
{ this.renderGeneralMenu() }
|
||||
{ this.renderNotificationsMenu(isActive) }
|
||||
</Button>
|
||||
}
|
||||
</RovingTabIndexWrapper>
|
||||
|
|
|
@ -1,91 +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.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useContext } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import VoiceChannelStore, { VoiceChannelEvent } from "../../../stores/VoiceChannelStore";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
||||
const _VoiceChannelRadio: FC<{ roomId: string }> = ({ roomId }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const room = cli.getRoom(roomId);
|
||||
const store = VoiceChannelStore.instance;
|
||||
|
||||
const [audioMuted, setAudioMuted] = useState<boolean>(store.audioMuted);
|
||||
const [videoMuted, setVideoMuted] = useState<boolean>(store.videoMuted);
|
||||
|
||||
useEventEmitter(store, VoiceChannelEvent.MuteAudio, () => setAudioMuted(true));
|
||||
useEventEmitter(store, VoiceChannelEvent.UnmuteAudio, () => setAudioMuted(false));
|
||||
useEventEmitter(store, VoiceChannelEvent.MuteVideo, () => setVideoMuted(true));
|
||||
useEventEmitter(store, VoiceChannelEvent.UnmuteVideo, () => setVideoMuted(false));
|
||||
|
||||
return <div className="mx_VoiceChannelRadio">
|
||||
<div className="mx_VoiceChannelRadio_statusBar">
|
||||
<DecoratedRoomAvatar room={room} avatarSize={36} />
|
||||
<div className="mx_VoiceChannelRadio_titleContainer">
|
||||
<div className="mx_VoiceChannelRadio_status">{ _t("Connected") }</div>
|
||||
<div className="mx_VoiceChannelRadio_name">{ room.name }</div>
|
||||
</div>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_VoiceChannelRadio_disconnectButton"
|
||||
title={_t("Disconnect")}
|
||||
onClick={() => store.disconnect()}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_VoiceChannelRadio_controlBar">
|
||||
<AccessibleButton
|
||||
className={classNames({
|
||||
"mx_VoiceChannelRadio_videoButton": true,
|
||||
"mx_VoiceChannelRadio_button_active": !videoMuted,
|
||||
})}
|
||||
onClick={() => videoMuted ? store.unmuteVideo() : store.muteVideo()}
|
||||
>
|
||||
{ videoMuted ? _t("Video off") : _t("Video") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={classNames({
|
||||
"mx_VoiceChannelRadio_audioButton": true,
|
||||
"mx_VoiceChannelRadio_button_active": !audioMuted,
|
||||
})}
|
||||
onClick={() => audioMuted ? store.unmuteAudio() : store.muteAudio()}
|
||||
>
|
||||
{ audioMuted ? _t("Mic off") : _t("Mic") }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const VoiceChannelRadio: FC<{}> = () => {
|
||||
const store = VoiceChannelStore.instance;
|
||||
|
||||
const [activeChannel, setActiveChannel] = useState<string>(VoiceChannelStore.instance.roomId);
|
||||
useEventEmitter(store, VoiceChannelEvent.Connect, () =>
|
||||
setActiveChannel(VoiceChannelStore.instance.roomId),
|
||||
);
|
||||
useEventEmitter(store, VoiceChannelEvent.Disconnect, () =>
|
||||
setActiveChannel(null),
|
||||
);
|
||||
|
||||
return activeChannel ? <_VoiceChannelRadio roomId={activeChannel} /> : null;
|
||||
};
|
||||
|
||||
export default VoiceChannelRadio;
|
Loading…
Add table
Add a link
Reference in a new issue