Merge pull request #45 from superhero-com/fix/disabled-message
feat: adds thresholds to preview screens & chat barpull/27073/head
commit
ab8f4f4686
|
@ -2,7 +2,9 @@
|
|||
"src/components/views/auth/AuthFooter.tsx": "src/components/views/auth/VectorAuthFooter.tsx",
|
||||
"src/components/views/auth/AuthHeaderLogo.tsx": "src/components/views/auth/VectorAuthHeaderLogo.tsx",
|
||||
"src/components/views/auth/AuthPage.tsx": "src/components/views/auth/VectorAuthPage.tsx",
|
||||
"src/components/views/rooms/MessageComposer.tsx": "src/components/views/rooms/MessageComposer.tsx",
|
||||
"src/components/views/rooms/RoomTile.tsx": "src/components/views/rooms/RoomTile.tsx",
|
||||
"src/components/views/rooms/RoomPreviewBar.tsx": "src/components/views/rooms/RoomPreviewBar.tsx",
|
||||
"src/components/views/rooms/NewRoomIntro.tsx": "src/components/views/rooms/NewRoomIntro.tsx",
|
||||
"src/components/views/elements/RoomName.tsx": "src/components/views/elements/RoomName.tsx",
|
||||
"src/components/views/dialogs/spotlight/PublicRoomResultDetails.tsx": "src/components/views/dialogs/spotlight/PublicRoomResultDetails.tsx",
|
||||
|
|
|
@ -46,5 +46,6 @@
|
|||
"participant_limit": 8,
|
||||
"brand": "Element Call"
|
||||
},
|
||||
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
|
||||
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
|
||||
"community_bot_user_id": "@communitybot:superhero.com"
|
||||
}
|
||||
|
|
15
src/atoms.ts
15
src/atoms.ts
|
@ -1,3 +1,18 @@
|
|||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
type TokenThreshold = {
|
||||
threshold: string;
|
||||
symbol: string;
|
||||
};
|
||||
|
||||
export type BareUser = {
|
||||
userId: string;
|
||||
rawDisplayName: string;
|
||||
};
|
||||
|
||||
export const verifiedAccountsAtom = atomWithStorage<Record<string, string>>("VERIFIED_ACCOUNTS", {});
|
||||
export const minimumTokenThresholdAtom = atomWithStorage<Record<string, TokenThreshold>>("TOKEN_THRESHOLD", {});
|
||||
export const communityBotAtom = atomWithStorage<BareUser>("COMMUNITY_BOT", {
|
||||
userId: "",
|
||||
rawDisplayName: "",
|
||||
});
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { useAtom } from "jotai";
|
||||
import React, { ReactElement } from "react";
|
||||
|
||||
import { minimumTokenThresholdAtom } from "../../../atoms";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { cleanRoomName } from "../../../hooks/useVerifiedRoom";
|
||||
|
||||
export function CommunityRoomPeekMessage({ roomName }: { roomName: string }): ReactElement {
|
||||
const [allTokens] = useAtom(minimumTokenThresholdAtom);
|
||||
const cleanedRoomName = cleanRoomName(roomName);
|
||||
|
||||
const tokenThreshold = allTokens[cleanedRoomName];
|
||||
|
||||
return (
|
||||
<h3>
|
||||
{_t("room|no_peek_join_prompt_community", { roomName: cleanedRoomName })}{" "}
|
||||
{tokenThreshold ? _t("room|no_peek_join_prompt_community_threshold", tokenThreshold) : ""}
|
||||
</h3>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { useAtom } from "jotai";
|
||||
import React from "react";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { minimumTokenThresholdAtom } from "../../../atoms";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useVerifiedRoom } from "../../../hooks/useVerifiedRoom";
|
||||
import { MessageCommunityBotButton } from "./MessageButton";
|
||||
|
||||
export function DisabledMessageField({ room }: { room: Room }): JSX.Element {
|
||||
const [allTokens] = useAtom(minimumTokenThresholdAtom);
|
||||
const { isTokenGatedRoom, isCommunityRoom } = useVerifiedRoom(room);
|
||||
|
||||
let tokenThreshold = allTokens[room.name];
|
||||
if (!tokenThreshold) {
|
||||
const tokenName = room.name.match(/\[TG] (.*) \(ct_.*\)/)?.[1];
|
||||
if (isTokenGatedRoom && tokenName) {
|
||||
tokenThreshold = {
|
||||
threshold: "1",
|
||||
symbol: tokenName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenThreshold) {
|
||||
return (
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{_t("composer|no_perms_token_notice", tokenThreshold)}
|
||||
{isCommunityRoom ? (
|
||||
<>
|
||||
<span style={{ marginLeft: "1rem", display: "block" }} />
|
||||
<MessageCommunityBotButton text="Get room tokens" />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{_t("composer|no_perms_notice")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import MatrixClientContext from "matrix-react-sdk/src/contexts/MatrixClientContext";
|
||||
import AccessibleButton from "matrix-react-sdk/src/components/views/elements/AccessibleButton";
|
||||
import { MatrixClient, RoomMember, User } from "matrix-js-sdk/src/matrix";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "matrix-react-sdk/src/utils/direct-messages";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import { Member } from "../right_panel/UserInfo";
|
||||
import { Icon as SendMessage } from "../../../../res/themes/superhero/img/icons/send.svg";
|
||||
import { BareUser, communityBotAtom } from "../../../atoms";
|
||||
|
||||
/**
|
||||
* Converts the member to a DirectoryMember and starts a DM with them.
|
||||
*/
|
||||
async function openDmForUser(matrixClient: MatrixClient, user: Member | BareUser): Promise<void> {
|
||||
const avatarUrl = user instanceof User ? user.avatarUrl : user instanceof RoomMember ? user.getMxcAvatarUrl() : "";
|
||||
const startDmUser = new DirectoryMember({
|
||||
user_id: user.userId,
|
||||
display_name: user.rawDisplayName,
|
||||
avatar_url: avatarUrl,
|
||||
});
|
||||
await startDmOnFirstMessage(matrixClient, [startDmUser]);
|
||||
}
|
||||
|
||||
export const MessageButton = ({
|
||||
member,
|
||||
text = "Send Message",
|
||||
}: {
|
||||
member: Member | BareUser;
|
||||
text?: string;
|
||||
}): JSX.Element => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
await openDmForUser(cli, member);
|
||||
setBusy(false);
|
||||
}}
|
||||
className="mx_UserInfo_field"
|
||||
disabled={busy}
|
||||
>
|
||||
<SendMessage width="16px" height="16px" />
|
||||
<span style={{ marginLeft: "5px" }}>{text}</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const MessageCommunityBotButton = ({ text = "Send Message" }: { text?: string }): JSX.Element => {
|
||||
const [communityBot] = useAtom(communityBotAtom);
|
||||
|
||||
return <MessageButton member={communityBot} text={text} />;
|
||||
};
|
|
@ -78,13 +78,12 @@ import { IRightPanelCardState } from "matrix-react-sdk/src/stores/right-panel/Ri
|
|||
import UserIdentifierCustomisations from "matrix-react-sdk/src/customisations/UserIdentifier";
|
||||
import PosthogTrackers from "matrix-react-sdk/src/PosthogTrackers";
|
||||
import { ViewRoomPayload } from "matrix-react-sdk/src/dispatcher/payloads/ViewRoomPayload";
|
||||
import { DirectoryMember, startDmOnFirstMessage } from "matrix-react-sdk/src/utils/direct-messages";
|
||||
import { SdkContextClass } from "matrix-react-sdk/src/contexts/SDKContext";
|
||||
import { asyncSome } from "matrix-react-sdk/src/utils/arrays";
|
||||
import UIStore from "matrix-react-sdk/src/stores/UIStore";
|
||||
|
||||
import { UserVerifiedBadge } from "../elements/UserVerifiedBadge";
|
||||
import { Icon as SendMessage } from "../../../../res/themes/superhero/img/icons/send.svg";
|
||||
import { MessageButton } from "../elements/MessageButton";
|
||||
|
||||
export interface IDevice extends Device {
|
||||
ambiguous?: boolean;
|
||||
|
@ -133,19 +132,6 @@ export const getE2EStatus = async (
|
|||
return anyDeviceUnverified ? E2EStatus.Warning : E2EStatus.Verified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the member to a DirectoryMember and starts a DM with them.
|
||||
*/
|
||||
async function openDmForUser(matrixClient: MatrixClient, user: Member): Promise<void> {
|
||||
const avatarUrl = user instanceof User ? user.avatarUrl : user.getMxcAvatarUrl();
|
||||
const startDmUser = new DirectoryMember({
|
||||
user_id: user.userId,
|
||||
display_name: user.rawDisplayName,
|
||||
avatar_url: avatarUrl,
|
||||
});
|
||||
await startDmOnFirstMessage(matrixClient, [startDmUser]);
|
||||
}
|
||||
|
||||
type SetUpdating = (updating: boolean) => void;
|
||||
|
||||
function useHasCrossSigningKeys(
|
||||
|
@ -359,28 +345,6 @@ function DevicesSection({
|
|||
);
|
||||
}
|
||||
|
||||
const MessageButton = ({ member }: { member: Member }): JSX.Element => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
return (
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={async (): Promise<void> => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
await openDmForUser(cli, member);
|
||||
setBusy(false);
|
||||
}}
|
||||
className="mx_UserInfo_field"
|
||||
disabled={busy}
|
||||
>
|
||||
<SendMessage width="16px" height="16px" />
|
||||
<span style={{ marginLeft: "5px" }}>Send Message</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserOptionsSection: React.FC<{
|
||||
member: Member;
|
||||
isIgnored: boolean;
|
||||
|
|
|
@ -0,0 +1,667 @@
|
|||
/*
|
||||
Copyright 2015 - 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, { createRef, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
IEventRelation,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomMember,
|
||||
EventType,
|
||||
THREAD_RELATION_TYPE,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg";
|
||||
import { ButtonEvent } from "matrix-react-sdk/src/components/views/elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "matrix-react-sdk/src/components/views/elements/AccessibleTooltipButton";
|
||||
import { MatrixClientProps, withMatrixClientHOC } from "matrix-react-sdk/src/contexts/MatrixClientContext";
|
||||
import ResizeNotifier from "matrix-react-sdk/src/utils/ResizeNotifier";
|
||||
import { E2EStatus } from "matrix-react-sdk/src/utils/ShieldUtils";
|
||||
import { makeRoomPermalink, RoomPermalinkCreator } from "matrix-react-sdk/src/utils/permalinks/Permalinks";
|
||||
import VoiceRecordComposerTile from "matrix-react-sdk/src/components/views/rooms/VoiceRecordComposerTile";
|
||||
import { VoiceMessageRecording } from "matrix-react-sdk/src/audio/VoiceMessageRecording";
|
||||
import RoomContext from "matrix-react-sdk/src/contexts/RoomContext";
|
||||
import { UPDATE_EVENT } from "matrix-react-sdk/src/stores/AsyncStore";
|
||||
import { VoiceRecordingStore } from "matrix-react-sdk/src/stores/VoiceRecordingStore";
|
||||
import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore";
|
||||
import { Features } from "matrix-react-sdk/src/settings/Settings";
|
||||
import { RecordingState } from "matrix-react-sdk/src/audio/VoiceRecording";
|
||||
import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
|
||||
import UIStore, { UI_EVENTS } from "matrix-react-sdk/src/stores/UIStore";
|
||||
import { ActionPayload } from "matrix-react-sdk/src/dispatcher/payloads";
|
||||
import { Action } from "matrix-react-sdk/src/dispatcher/actions";
|
||||
import { SettingUpdatedPayload } from "matrix-react-sdk/src/dispatcher/payloads/SettingUpdatedPayload";
|
||||
import { ViewRoomPayload } from "matrix-react-sdk/src/dispatcher/payloads/ViewRoomPayload";
|
||||
import { ComposerInsertPayload } from "matrix-react-sdk/src/dispatcher/payloads/ComposerInsertPayload";
|
||||
import {
|
||||
getConversionFunctions,
|
||||
sendMessage,
|
||||
SendWysiwygComposer,
|
||||
} from "matrix-react-sdk/src/components/views/rooms/wysiwyg_composer";
|
||||
import EditorModel from "matrix-react-sdk/src/editor/model";
|
||||
import { isLocalRoom } from "matrix-react-sdk/src/utils/localRoom/isLocalRoom";
|
||||
import { aboveLeftOf, MenuProps } from "matrix-react-sdk/src/components/structures/ContextMenu";
|
||||
import { SdkContextClass } from "matrix-react-sdk/src/contexts/SDKContext";
|
||||
import { VoiceBroadcastInfoState } from "matrix-react-sdk/src/voice-broadcast";
|
||||
import { createCantStartVoiceMessageBroadcastDialog } from "matrix-react-sdk/src/components/views/dialogs/CantStartVoiceMessageBroadcastDialog";
|
||||
import E2EIcon from "matrix-react-sdk/src/components/views/rooms/E2EIcon";
|
||||
import SendMessageComposer, {
|
||||
SendMessageComposer as SendMessageComposerClass,
|
||||
} from "matrix-react-sdk/src/components/views/rooms/SendMessageComposer";
|
||||
import Tooltip, { Alignment } from "matrix-react-sdk/src/components/views/elements/Tooltip";
|
||||
import { formatTimeLeft } from "matrix-react-sdk/src/DateUtils";
|
||||
import Stickerpicker from "matrix-react-sdk/src/components/views/rooms/Stickerpicker";
|
||||
import ReplyPreview from "matrix-react-sdk/src/components/views/rooms/ReplyPreview";
|
||||
import MessageComposerButtons from "matrix-react-sdk/src/components/views/rooms/MessageComposerButtons";
|
||||
import { UIFeature } from "matrix-react-sdk/src/settings/UIFeature";
|
||||
import { setUpVoiceBroadcastPreRecording } from "matrix-react-sdk/src/voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
||||
import { Icon as RoomReplacedIcon } from "matrix-react-sdk/res/img/room_replaced.svg";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { DisabledMessageField } from "../elements/DisabledMessageField";
|
||||
|
||||
let instanceCount = 0;
|
||||
|
||||
interface ISendButtonProps {
|
||||
onClick: (ev: ButtonEvent) => void;
|
||||
title?: string; // defaults to something generic
|
||||
}
|
||||
|
||||
function SendButton(props: ISendButtonProps): JSX.Element {
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_sendMessage"
|
||||
onClick={props.onClick}
|
||||
title={props.title ?? _t("composer|send_button_title")}
|
||||
data-testid="sendmessagebtn"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface IProps extends MatrixClientProps {
|
||||
room: Room;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
permalinkCreator?: RoomPermalinkCreator;
|
||||
replyToEvent?: MatrixEvent;
|
||||
relation?: IEventRelation;
|
||||
e2eStatus?: E2EStatus;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
composerContent: string;
|
||||
isComposerEmpty: boolean;
|
||||
haveRecording: boolean;
|
||||
recordingTimeLeftSeconds?: number;
|
||||
me?: RoomMember;
|
||||
isMenuOpen: boolean;
|
||||
isStickerPickerOpen: boolean;
|
||||
showStickersButton: boolean;
|
||||
showPollsButton: boolean;
|
||||
showVoiceBroadcastButton: boolean;
|
||||
isWysiwygLabEnabled: boolean;
|
||||
isRichTextEnabled: boolean;
|
||||
initialComposerContent: string;
|
||||
}
|
||||
|
||||
export class MessageComposer extends React.Component<IProps, IState> {
|
||||
private tooltipId = `mx_MessageComposer_${Math.random()}`;
|
||||
private dispatcherRef?: string;
|
||||
private messageComposerInput = createRef<SendMessageComposerClass>();
|
||||
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
|
||||
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||
private instanceId: number;
|
||||
|
||||
private _voiceRecording: Optional<VoiceMessageRecording>;
|
||||
|
||||
public static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
|
||||
public static defaultProps = {
|
||||
compact: false,
|
||||
showVoiceBroadcastButton: false,
|
||||
isRichTextEnabled: true,
|
||||
};
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate);
|
||||
|
||||
this.state = {
|
||||
isComposerEmpty: true,
|
||||
composerContent: "",
|
||||
haveRecording: false,
|
||||
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
|
||||
isMenuOpen: false,
|
||||
isStickerPickerOpen: false,
|
||||
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
|
||||
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
|
||||
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
|
||||
isWysiwygLabEnabled: SettingsStore.getValue<boolean>("feature_wysiwyg_composer"),
|
||||
isRichTextEnabled: true,
|
||||
initialComposerContent: "",
|
||||
};
|
||||
|
||||
this.instanceId = instanceCount++;
|
||||
|
||||
SettingsStore.monitorSetting("MessageComposerInput.showStickersButton", null);
|
||||
SettingsStore.monitorSetting("MessageComposerInput.showPollsButton", null);
|
||||
SettingsStore.monitorSetting(Features.VoiceBroadcast, null);
|
||||
SettingsStore.monitorSetting("feature_wysiwyg_composer", null);
|
||||
}
|
||||
|
||||
private get voiceRecording(): Optional<VoiceMessageRecording> {
|
||||
return this._voiceRecording;
|
||||
}
|
||||
|
||||
private set voiceRecording(rec: Optional<VoiceMessageRecording>) {
|
||||
if (this._voiceRecording) {
|
||||
this._voiceRecording.off(RecordingState.Started, this.onRecordingStarted);
|
||||
this._voiceRecording.off(RecordingState.EndingSoon, this.onRecordingEndingSoon);
|
||||
}
|
||||
|
||||
this._voiceRecording = rec;
|
||||
|
||||
if (rec) {
|
||||
// Delay saying we have a recording until it is started, as we might not yet
|
||||
// have A/V permissions
|
||||
rec.on(RecordingState.Started, this.onRecordingStarted);
|
||||
|
||||
// We show a little heads up that the recording is about to automatically end soon. The 3s
|
||||
// display time is completely arbitrary.
|
||||
rec.on(RecordingState.EndingSoon, this.onRecordingEndingSoon);
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
this.waitForOwnMember();
|
||||
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current!);
|
||||
UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize);
|
||||
this.updateRecordingState(); // grab any cached recordings
|
||||
}
|
||||
|
||||
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry): void => {
|
||||
if (type === UI_EVENTS.Resize) {
|
||||
const { narrow } = this.context;
|
||||
this.setState({
|
||||
isMenuOpen: !narrow ? false : this.state.isMenuOpen,
|
||||
isStickerPickerOpen: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
switch (payload.action) {
|
||||
case "reply_to_event":
|
||||
if (payload.context === this.context.timelineRenderingType) {
|
||||
// add a timeout for the reply preview to be rendered, so
|
||||
// that the ScrollPanel listening to the resizeNotifier can
|
||||
// correctly measure it's new height and scroll down to keep
|
||||
// at the bottom if it already is
|
||||
window.setTimeout(() => {
|
||||
this.props.resizeNotifier.notifyTimelineHeightChanged();
|
||||
}, 100);
|
||||
}
|
||||
break;
|
||||
|
||||
case Action.SettingUpdated: {
|
||||
const settingUpdatedPayload = payload as SettingUpdatedPayload;
|
||||
switch (settingUpdatedPayload.settingName) {
|
||||
case "MessageComposerInput.showStickersButton": {
|
||||
const showStickersButton = SettingsStore.getValue("MessageComposerInput.showStickersButton");
|
||||
if (this.state.showStickersButton !== showStickersButton) {
|
||||
this.setState({ showStickersButton });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageComposerInput.showPollsButton": {
|
||||
const showPollsButton = SettingsStore.getValue("MessageComposerInput.showPollsButton");
|
||||
if (this.state.showPollsButton !== showPollsButton) {
|
||||
this.setState({ showPollsButton });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Features.VoiceBroadcast: {
|
||||
if (this.state.showVoiceBroadcastButton !== settingUpdatedPayload.newValue) {
|
||||
this.setState({ showVoiceBroadcastButton: !!settingUpdatedPayload.newValue });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "feature_wysiwyg_composer": {
|
||||
if (this.state.isWysiwygLabEnabled !== settingUpdatedPayload.newValue) {
|
||||
this.setState({ isWysiwygLabEnabled: Boolean(settingUpdatedPayload.newValue) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private waitForOwnMember(): void {
|
||||
// If we have the member already, do that
|
||||
const me = this.props.room.getMember(MatrixClientPeg.safeGet().getUserId()!);
|
||||
if (me) {
|
||||
this.setState({ me });
|
||||
return;
|
||||
}
|
||||
// Otherwise, wait for member loading to finish and then update the member for the avatar.
|
||||
// The members should already be loading, and loadMembersIfNeeded
|
||||
// will return the promise for the existing operation
|
||||
this.props.room.loadMembersIfNeeded().then(() => {
|
||||
const me = this.props.room.getMember(MatrixClientPeg.safeGet().getSafeUserId()) ?? undefined;
|
||||
this.setState({ me });
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
|
||||
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
|
||||
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
|
||||
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);
|
||||
|
||||
// clean up our listeners by setting our cached recording to falsy (see internal setter)
|
||||
this.voiceRecording = null;
|
||||
}
|
||||
|
||||
private onTombstoneClick = (ev: ButtonEvent): void => {
|
||||
ev.preventDefault();
|
||||
|
||||
const replacementRoomId = this.context.tombstone?.getContent()["replacement_room"];
|
||||
const replacementRoom = MatrixClientPeg.safeGet().getRoom(replacementRoomId);
|
||||
let createEventId: string | undefined;
|
||||
if (replacementRoom) {
|
||||
const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (createEvent?.getId()) createEventId = createEvent.getId();
|
||||
}
|
||||
|
||||
const sender = this.context.tombstone?.getSender();
|
||||
const viaServers = sender ? [sender.split(":").slice(1).join(":")] : undefined;
|
||||
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
highlighted: true,
|
||||
event_id: createEventId,
|
||||
room_id: replacementRoomId,
|
||||
auto_join: true,
|
||||
// Try to join via the server that sent the event. This converts @something:example.org
|
||||
// into a server domain by splitting on colons and ignoring the first entry ("@something").
|
||||
via_servers: viaServers,
|
||||
metricsTrigger: "Tombstone",
|
||||
metricsViaKeyboard: ev.type !== "click",
|
||||
});
|
||||
};
|
||||
|
||||
private renderPlaceholderText = (): string => {
|
||||
if (this.props.replyToEvent) {
|
||||
const replyingToThread = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name;
|
||||
if (replyingToThread && this.props.e2eStatus) {
|
||||
return _t("composer|placeholder_thread_encrypted");
|
||||
} else if (replyingToThread) {
|
||||
return _t("composer|placeholder_thread");
|
||||
} else if (this.props.e2eStatus) {
|
||||
return _t("composer|placeholder_reply_encrypted");
|
||||
} else {
|
||||
return _t("composer|placeholder_reply");
|
||||
}
|
||||
} else {
|
||||
if (this.props.e2eStatus) {
|
||||
return _t("composer|placeholder_encrypted");
|
||||
} else {
|
||||
return _t("composer|placeholder");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private addEmoji = (emoji: string): boolean => {
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
text: emoji,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
private sendMessage = async (): Promise<void> => {
|
||||
if (this.state.haveRecording && this.voiceRecordingButton.current) {
|
||||
// There shouldn't be any text message to send when a voice recording is active, so
|
||||
// just send out the voice recording.
|
||||
await this.voiceRecordingButton.current?.send();
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageComposerInput.current?.sendMessage();
|
||||
|
||||
if (this.state.isWysiwygLabEnabled) {
|
||||
const { permalinkCreator, relation, replyToEvent } = this.props;
|
||||
const composerContent = this.state.composerContent;
|
||||
this.setState({ composerContent: "", initialComposerContent: "" });
|
||||
dis.dispatch({
|
||||
action: Action.ClearAndFocusSendMessageComposer,
|
||||
timelineRenderingType: this.context.timelineRenderingType,
|
||||
});
|
||||
await sendMessage(composerContent, this.state.isRichTextEnabled, {
|
||||
mxClient: this.props.mxClient,
|
||||
roomContext: this.context,
|
||||
permalinkCreator,
|
||||
relation,
|
||||
replyToEvent,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onChange = (model: EditorModel): void => {
|
||||
this.setState({
|
||||
isComposerEmpty: model.isEmpty,
|
||||
});
|
||||
};
|
||||
|
||||
private onWysiwygChange = (content: string): void => {
|
||||
this.setState({
|
||||
composerContent: content,
|
||||
isComposerEmpty: content?.length === 0,
|
||||
});
|
||||
};
|
||||
|
||||
private onRichTextToggle = async (): Promise<void> => {
|
||||
const { richToPlain, plainToRich } = await getConversionFunctions();
|
||||
|
||||
const { isRichTextEnabled, composerContent } = this.state;
|
||||
const convertedContent = isRichTextEnabled
|
||||
? await richToPlain(composerContent, false)
|
||||
: await plainToRich(composerContent, false);
|
||||
|
||||
this.setState({
|
||||
isRichTextEnabled: !isRichTextEnabled,
|
||||
composerContent: convertedContent,
|
||||
initialComposerContent: convertedContent,
|
||||
});
|
||||
};
|
||||
|
||||
private onVoiceStoreUpdate = (): void => {
|
||||
this.updateRecordingState();
|
||||
};
|
||||
|
||||
private updateRecordingState(): void {
|
||||
const voiceRecordingId = VoiceRecordingStore.getVoiceRecordingId(this.props.room, this.props.relation);
|
||||
this.voiceRecording = VoiceRecordingStore.instance.getActiveRecording(voiceRecordingId);
|
||||
if (this.voiceRecording) {
|
||||
// If the recording has already started, it's probably a cached one.
|
||||
if (this.voiceRecording.hasRecording && !this.voiceRecording.isRecording) {
|
||||
this.setState({ haveRecording: true });
|
||||
}
|
||||
|
||||
// Note: Listeners for recording states are set by the `this.voiceRecording` setter.
|
||||
} else {
|
||||
this.setState({ haveRecording: false });
|
||||
}
|
||||
}
|
||||
|
||||
private onRecordingStarted = (): void => {
|
||||
// update the recording instance, just in case
|
||||
const voiceRecordingId = VoiceRecordingStore.getVoiceRecordingId(this.props.room, this.props.relation);
|
||||
this.voiceRecording = VoiceRecordingStore.instance.getActiveRecording(voiceRecordingId);
|
||||
this.setState({
|
||||
haveRecording: !!this.voiceRecording,
|
||||
});
|
||||
};
|
||||
|
||||
private onRecordingEndingSoon = ({ secondsLeft }: { secondsLeft: number }): void => {
|
||||
this.setState({ recordingTimeLeftSeconds: secondsLeft });
|
||||
window.setTimeout(() => this.setState({ recordingTimeLeftSeconds: undefined }), 3000);
|
||||
};
|
||||
|
||||
private setStickerPickerOpen = (isStickerPickerOpen: boolean): void => {
|
||||
this.setState({
|
||||
isStickerPickerOpen,
|
||||
isMenuOpen: false,
|
||||
});
|
||||
};
|
||||
|
||||
private toggleStickerPickerOpen = (): void => {
|
||||
this.setStickerPickerOpen(!this.state.isStickerPickerOpen);
|
||||
};
|
||||
|
||||
private toggleButtonMenu = (): void => {
|
||||
this.setState({
|
||||
isMenuOpen: !this.state.isMenuOpen,
|
||||
});
|
||||
};
|
||||
|
||||
private get showStickersButton(): boolean {
|
||||
return this.state.showStickersButton && !isLocalRoom(this.props.room);
|
||||
}
|
||||
|
||||
private getMenuPosition(): MenuProps | undefined {
|
||||
if (this.ref.current) {
|
||||
const hasFormattingButtons = this.state.isWysiwygLabEnabled && this.state.isRichTextEnabled;
|
||||
const contentRect = this.ref.current.getBoundingClientRect();
|
||||
// Here we need to remove the all the extra space above the editor
|
||||
// Instead of doing a querySelector or pass a ref to find the compute the height formatting buttons
|
||||
// We are using an arbitrary value, the formatting buttons height doesn't change during the lifecycle of the component
|
||||
// It's easier to just use a constant here instead of an over-engineering way to find the height
|
||||
const heightToRemove = hasFormattingButtons ? 36 : 0;
|
||||
const fixedRect = new DOMRect(
|
||||
contentRect.x,
|
||||
contentRect.y + heightToRemove,
|
||||
contentRect.width,
|
||||
contentRect.height - heightToRemove,
|
||||
);
|
||||
return aboveLeftOf(fixedRect);
|
||||
}
|
||||
}
|
||||
|
||||
private onRecordStartEndClick = (): void => {
|
||||
const currentBroadcastRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent();
|
||||
|
||||
if (currentBroadcastRecording && currentBroadcastRecording.getState() !== VoiceBroadcastInfoState.Stopped) {
|
||||
createCantStartVoiceMessageBroadcastDialog();
|
||||
} else {
|
||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||
}
|
||||
|
||||
if (this.context.narrow) {
|
||||
this.toggleButtonMenu();
|
||||
}
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
|
||||
const e2eIcon = hasE2EIcon && (
|
||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus!} className="mx_MessageComposer_e2eIcon" />
|
||||
);
|
||||
|
||||
const controls: ReactNode[] = [];
|
||||
const menuPosition = this.getMenuPosition();
|
||||
|
||||
const canSendMessages = this.context.canSendMessages && !this.context.tombstone;
|
||||
let composer: ReactNode;
|
||||
if (canSendMessages) {
|
||||
if (this.state.isWysiwygLabEnabled && menuPosition) {
|
||||
composer = (
|
||||
<SendWysiwygComposer
|
||||
key="controls_input"
|
||||
disabled={this.state.haveRecording}
|
||||
onChange={this.onWysiwygChange}
|
||||
onSend={this.sendMessage}
|
||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||
initialContent={this.state.initialComposerContent}
|
||||
e2eStatus={this.props.e2eStatus}
|
||||
menuPosition={menuPosition}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
eventRelation={this.props.relation}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
composer = (
|
||||
<SendMessageComposer
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
room={this.props.room}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
disabled={this.state.haveRecording}
|
||||
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
controls.push(
|
||||
<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={this.voiceRecordingButton}
|
||||
room={this.props.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
/>,
|
||||
);
|
||||
} else if (this.context.tombstone) {
|
||||
const replacementRoomId = this.context.tombstone.getContent()["replacement_room"];
|
||||
|
||||
const continuesLink = replacementRoomId ? (
|
||||
<a
|
||||
href={makeRoomPermalink(MatrixClientPeg.safeGet(), replacementRoomId)}
|
||||
className="mx_MessageComposer_roomReplaced_link"
|
||||
onClick={this.onTombstoneClick}
|
||||
>
|
||||
{_t("composer|room_upgraded_link")}
|
||||
</a>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
controls.push(
|
||||
<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<RoomReplacedIcon aria-hidden className="mx_MessageComposer_roomReplaced_icon" />
|
||||
<span className="mx_MessageComposer_roomReplaced_header">
|
||||
{_t("composer|room_upgraded_notice")}
|
||||
</span>
|
||||
<br />
|
||||
{continuesLink}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
controls.push(<DisabledMessageField room={this.props.room} key="controls_error" />);
|
||||
}
|
||||
|
||||
let recordingTooltip: JSX.Element | undefined;
|
||||
if (this.state.recordingTimeLeftSeconds) {
|
||||
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
|
||||
recordingTooltip = (
|
||||
<Tooltip id={this.tooltipId} label={formatTimeLeft(secondsLeft)} alignment={Alignment.Top} />
|
||||
);
|
||||
}
|
||||
|
||||
const threadId =
|
||||
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
|
||||
|
||||
controls.push(
|
||||
<Stickerpicker
|
||||
room={this.props.room}
|
||||
threadId={threadId}
|
||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||
setStickerPickerOpen={this.setStickerPickerOpen}
|
||||
menuPosition={menuPosition}
|
||||
key="stickers"
|
||||
/>,
|
||||
);
|
||||
|
||||
const showSendButton = canSendMessages && (!this.state.isComposerEmpty || this.state.haveRecording);
|
||||
|
||||
const classes = classNames({
|
||||
"mx_MessageComposer": true,
|
||||
"mx_MessageComposer--compact": this.props.compact,
|
||||
"mx_MessageComposer_e2eStatus": hasE2EIcon,
|
||||
"mx_MessageComposer_wysiwyg": this.state.isWysiwygLabEnabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
ref={this.ref}
|
||||
aria-describedby={this.state.recordingTimeLeftSeconds ? this.tooltipId : undefined}
|
||||
role="region"
|
||||
aria-label={_t("a11y|message_composer")}
|
||||
>
|
||||
{recordingTooltip}
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<ReplyPreview
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>
|
||||
<div className="mx_MessageComposer_row">
|
||||
{e2eIcon}
|
||||
{composer}
|
||||
<div className="mx_MessageComposer_actions">
|
||||
{controls}
|
||||
{canSendMessages && (
|
||||
<MessageComposerButtons
|
||||
addEmoji={this.addEmoji}
|
||||
haveRecording={this.state.haveRecording}
|
||||
isMenuOpen={this.state.isMenuOpen}
|
||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||
menuPosition={menuPosition}
|
||||
relation={this.props.relation}
|
||||
onRecordStartEndClick={this.onRecordStartEndClick}
|
||||
setStickerPickerOpen={this.setStickerPickerOpen}
|
||||
showLocationButton={
|
||||
!window.electron && SettingsStore.getValue(UIFeature.LocationSharing)
|
||||
}
|
||||
showPollsButton={this.state.showPollsButton}
|
||||
showStickersButton={this.showStickersButton}
|
||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||
onComposerModeClick={this.onRichTextToggle}
|
||||
toggleButtonMenu={this.toggleButtonMenu}
|
||||
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
||||
onStartVoiceBroadcastClick={() => {
|
||||
setUpVoiceBroadcastPreRecording(
|
||||
this.props.room,
|
||||
MatrixClientPeg.safeGet(),
|
||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
|
||||
SdkContextClass.instance.voiceBroadcastRecordingsStore,
|
||||
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
||||
);
|
||||
this.toggleButtonMenu();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showSendButton && (
|
||||
<SendButton
|
||||
key="controls_send"
|
||||
onClick={this.sendMessage}
|
||||
title={
|
||||
this.state.haveRecording ? _t("composer|send_button_voice_message") : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MessageComposerWithMatrixClient = withMatrixClientHOC(MessageComposer);
|
||||
export default MessageComposerWithMatrixClient;
|
|
@ -0,0 +1,752 @@
|
|||
/*
|
||||
Copyright 2015-2021 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, { ChangeEvent, ReactNode } from "react";
|
||||
import {
|
||||
Room,
|
||||
RoomMember,
|
||||
EventType,
|
||||
RoomType,
|
||||
IJoinRuleEventContent,
|
||||
JoinRule,
|
||||
MatrixError,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import classNames from "classnames";
|
||||
import { RoomPreviewOpts, RoomViewLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import { Icon as AskToJoinIcon } from "matrix-react-sdk/res/img/element-icons/ask-to-join.svg";
|
||||
import { IOOBData } from "matrix-react-sdk/src/stores/ThreepidInviteStore";
|
||||
import { MatrixClientPeg } from "matrix-react-sdk/src/MatrixClientPeg";
|
||||
import IdentityAuthClient from "matrix-react-sdk/src/IdentityAuthClient";
|
||||
import { UserFriendlyError } from "matrix-react-sdk/src/languageHandler";
|
||||
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
|
||||
import { ModuleRunner } from "matrix-react-sdk/src/modules/ModuleRunner";
|
||||
import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore";
|
||||
import { UIFeature } from "matrix-react-sdk/src/settings/UIFeature";
|
||||
import Spinner from "matrix-react-sdk/src/components/views/elements/Spinner";
|
||||
import RoomAvatar from "matrix-react-sdk/src/components/views/avatars/RoomAvatar";
|
||||
import InviteReason from "matrix-react-sdk/src/components/views/elements/InviteReason";
|
||||
import AccessibleButton from "matrix-react-sdk/src/components/views/elements/AccessibleButton";
|
||||
import Field from "matrix-react-sdk/src/components/views/elements/Field";
|
||||
import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { isVerifiedRoom } from "../../../hooks/useVerifiedRoom";
|
||||
import { MessageCommunityBotButton } from "../elements/MessageButton";
|
||||
import { CommunityRoomPeekMessage } from "../elements/CommunityRoomPeekMessage";
|
||||
|
||||
const MemberEventHtmlReasonField = "io.element.html_reason";
|
||||
|
||||
enum MessageCase {
|
||||
NotLoggedIn = "NotLoggedIn",
|
||||
Joining = "Joining",
|
||||
Loading = "Loading",
|
||||
Rejecting = "Rejecting",
|
||||
Kicked = "Kicked",
|
||||
Banned = "Banned",
|
||||
OtherThreePIDError = "OtherThreePIDError",
|
||||
InvitedEmailNotFoundInAccount = "InvitedEmailNotFoundInAccount",
|
||||
InvitedEmailNoIdentityServer = "InvitedEmailNoIdentityServer",
|
||||
InvitedEmailMismatch = "InvitedEmailMismatch",
|
||||
Invite = "Invite",
|
||||
ViewingRoom = "ViewingRoom",
|
||||
RoomNotFound = "RoomNotFound",
|
||||
OtherError = "OtherError",
|
||||
PromptAskToJoin = "PromptAskToJoin",
|
||||
Knocked = "Knocked",
|
||||
RequestDenied = "requestDenied",
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
// if inviterName is specified, the preview bar will shown an invite to the room.
|
||||
// You should also specify onRejectClick if specifying inviterName
|
||||
inviterName?: string;
|
||||
|
||||
// If invited by 3rd party invite, the email address the invite was sent to
|
||||
invitedEmail?: string;
|
||||
|
||||
// For third party invites, information passed about the room out-of-band
|
||||
oobData?: IOOBData;
|
||||
|
||||
// For third party invites, a URL for a 3pid invite signing service
|
||||
signUrl?: string;
|
||||
|
||||
// A standard client/server API error object. If supplied, indicates that the
|
||||
// caller was unable to fetch details about the room for the given reason.
|
||||
error?: MatrixError;
|
||||
|
||||
canPreview?: boolean;
|
||||
previewLoading?: boolean;
|
||||
|
||||
// The id of the room to be previewed, if it is known.
|
||||
// (It may be unknown if we are waiting for an alias to be resolved.)
|
||||
roomId?: string;
|
||||
|
||||
// A `Room` object for the room to be previewed, if we have one.
|
||||
room?: Room;
|
||||
|
||||
loading?: boolean;
|
||||
joining?: boolean;
|
||||
rejecting?: boolean;
|
||||
// The alias that was used to access this room, if appropriate
|
||||
// If given, this will be how the room is referred to (eg.
|
||||
// in error messages).
|
||||
roomAlias?: string;
|
||||
|
||||
onJoinClick?(): void;
|
||||
onRejectClick?(): void;
|
||||
onRejectAndIgnoreClick?(): void;
|
||||
onForgetClick?(): void;
|
||||
|
||||
canAskToJoinAndMembershipIsLeave?: boolean;
|
||||
promptAskToJoin?: boolean;
|
||||
knocked?: boolean;
|
||||
onSubmitAskToJoin?(reason?: string): void;
|
||||
onCancelAskToJoin?(): void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
busy: boolean;
|
||||
accountEmails?: string[];
|
||||
invitedEmailMxid?: string;
|
||||
threePidFetchError?: MatrixError;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
||||
public static defaultProps = {
|
||||
onJoinClick(): void {},
|
||||
};
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.checkInvitedEmail();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
|
||||
this.checkInvitedEmail();
|
||||
}
|
||||
}
|
||||
|
||||
private async checkInvitedEmail(): Promise<void> {
|
||||
// If this is an invite and we've been told what email address was
|
||||
// invited, fetch the user's account emails and discovery bindings so we
|
||||
// can check them against the email that was invited.
|
||||
if (this.props.inviterName && this.props.invitedEmail) {
|
||||
this.setState({ busy: true });
|
||||
try {
|
||||
// Gather the account 3PIDs
|
||||
const account3pids = await MatrixClientPeg.safeGet().getThreePids();
|
||||
this.setState({
|
||||
accountEmails: account3pids.threepids.filter((b) => b.medium === "email").map((b) => b.address),
|
||||
});
|
||||
// If we have an IS connected, use that to lookup the email and
|
||||
// check the bound MXID.
|
||||
if (!MatrixClientPeg.safeGet().getIdentityServerUrl()) {
|
||||
this.setState({ busy: false });
|
||||
return;
|
||||
}
|
||||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = await authClient.getAccessToken();
|
||||
const result = await MatrixClientPeg.safeGet().lookupThreePid(
|
||||
"email",
|
||||
this.props.invitedEmail,
|
||||
identityAccessToken!,
|
||||
);
|
||||
if (!("mxid" in result)) {
|
||||
throw new UserFriendlyError("room|error_3pid_invite_email_lookup");
|
||||
}
|
||||
this.setState({ invitedEmailMxid: result.mxid });
|
||||
} catch (err) {
|
||||
this.setState({ threePidFetchError: err as MatrixError });
|
||||
}
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
}
|
||||
|
||||
private getMessageCase(): MessageCase {
|
||||
const isGuest = MatrixClientPeg.safeGet().isGuest();
|
||||
|
||||
if (isGuest) {
|
||||
return MessageCase.NotLoggedIn;
|
||||
}
|
||||
|
||||
const myMember = this.getMyMember();
|
||||
|
||||
if (myMember) {
|
||||
const previousMembership = myMember.events.member?.getPrevContent().membership;
|
||||
if (myMember.isKicked()) {
|
||||
if (previousMembership === "knock") {
|
||||
return MessageCase.RequestDenied;
|
||||
} else if (this.props.promptAskToJoin) {
|
||||
return MessageCase.PromptAskToJoin;
|
||||
}
|
||||
return MessageCase.Kicked;
|
||||
} else if (myMember.membership === "ban") {
|
||||
return MessageCase.Banned;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.joining) {
|
||||
return MessageCase.Joining;
|
||||
} else if (this.props.rejecting) {
|
||||
return MessageCase.Rejecting;
|
||||
} else if (this.props.loading || this.state.busy) {
|
||||
return MessageCase.Loading;
|
||||
} else if (this.props.knocked) {
|
||||
return MessageCase.Knocked;
|
||||
} else if (this.props.canAskToJoinAndMembershipIsLeave || this.props.promptAskToJoin) {
|
||||
return MessageCase.PromptAskToJoin;
|
||||
}
|
||||
|
||||
if (this.props.inviterName) {
|
||||
if (this.props.invitedEmail) {
|
||||
if (this.state.threePidFetchError) {
|
||||
return MessageCase.OtherThreePIDError;
|
||||
} else if (this.state.accountEmails && !this.state.accountEmails.includes(this.props.invitedEmail)) {
|
||||
return MessageCase.InvitedEmailNotFoundInAccount;
|
||||
} else if (!MatrixClientPeg.safeGet().getIdentityServerUrl()) {
|
||||
return MessageCase.InvitedEmailNoIdentityServer;
|
||||
} else if (this.state.invitedEmailMxid != MatrixClientPeg.safeGet().getUserId()) {
|
||||
return MessageCase.InvitedEmailMismatch;
|
||||
}
|
||||
}
|
||||
return MessageCase.Invite;
|
||||
} else if (this.props.error) {
|
||||
if ((this.props.error as MatrixError).errcode == "M_NOT_FOUND") {
|
||||
return MessageCase.RoomNotFound;
|
||||
} else {
|
||||
return MessageCase.OtherError;
|
||||
}
|
||||
} else {
|
||||
return MessageCase.ViewingRoom;
|
||||
}
|
||||
}
|
||||
|
||||
private getKickOrBanInfo(): { memberName?: string; reason?: string } {
|
||||
const myMember = this.getMyMember();
|
||||
if (!myMember) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const kickerUserId = myMember.events.member?.getSender();
|
||||
const kickerMember = kickerUserId ? this.props.room?.currentState.getMember(kickerUserId) : undefined;
|
||||
const memberName = kickerMember?.name ?? kickerUserId;
|
||||
const reason = myMember.events.member?.getContent().reason;
|
||||
return { memberName, reason };
|
||||
}
|
||||
|
||||
private joinRule(): JoinRule | null {
|
||||
return (
|
||||
this.props.room?.currentState
|
||||
.getStateEvents(EventType.RoomJoinRules, "")
|
||||
?.getContent<IJoinRuleEventContent>().join_rule ?? null
|
||||
);
|
||||
}
|
||||
|
||||
private getMyMember(): RoomMember | null {
|
||||
return this.props.room?.getMember(MatrixClientPeg.safeGet().getSafeUserId()) ?? null;
|
||||
}
|
||||
|
||||
private getInviteMember(): RoomMember | null {
|
||||
const { room } = this.props;
|
||||
if (!room) {
|
||||
return null;
|
||||
}
|
||||
const myUserId = MatrixClientPeg.safeGet().getSafeUserId();
|
||||
const inviteEvent = room.currentState.getMember(myUserId);
|
||||
if (!inviteEvent) {
|
||||
return null;
|
||||
}
|
||||
const inviterUserId = inviteEvent.events.member?.getSender();
|
||||
return inviterUserId ? room.currentState.getMember(inviterUserId) : null;
|
||||
}
|
||||
|
||||
private isDMInvite(): boolean {
|
||||
const myMember = this.getMyMember();
|
||||
if (!myMember) {
|
||||
return false;
|
||||
}
|
||||
const memberContent = myMember.events.member?.getContent();
|
||||
return memberContent?.membership === "invite" && memberContent.is_direct;
|
||||
}
|
||||
|
||||
private makeScreenAfterLogin(): { screen: string; params: Record<string, any> } {
|
||||
return {
|
||||
screen: "room",
|
||||
params: {
|
||||
email: this.props.invitedEmail,
|
||||
signurl: this.props.signUrl,
|
||||
room_name: this.props.oobData?.name ?? null,
|
||||
room_avatar_url: this.props.oobData?.avatarUrl ?? null,
|
||||
inviter_name: this.props.oobData?.inviterName ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private onLoginClick = (): void => {
|
||||
dis.dispatch({ action: "start_login", screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
};
|
||||
|
||||
private onRegisterClick = (): void => {
|
||||
dis.dispatch({ action: "start_registration", screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
};
|
||||
|
||||
private onChangeReason = (event: ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
this.setState({ reason: event.target.value });
|
||||
};
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const brand = SdkConfig.get().brand;
|
||||
const roomName = this.props.room?.name ?? this.props.roomAlias ?? "";
|
||||
const isSpace = this.props.room?.isSpaceRoom() ?? this.props.oobData?.roomType === RoomType.Space;
|
||||
|
||||
let showSpinner = false;
|
||||
let title: string | undefined;
|
||||
let subTitle: string | ReactNode[] | undefined;
|
||||
let reasonElement: JSX.Element | undefined;
|
||||
let primaryActionHandler: (() => void) | undefined;
|
||||
let primaryActionLabel: string | undefined;
|
||||
let secondaryActionHandler: (() => void) | undefined;
|
||||
let secondaryActionLabel: string | undefined;
|
||||
let footer: JSX.Element | undefined;
|
||||
const extraComponents: JSX.Element[] = [];
|
||||
|
||||
const { isCommunityRoom } = isVerifiedRoom(roomName);
|
||||
|
||||
const messageCase = this.getMessageCase();
|
||||
switch (messageCase) {
|
||||
case MessageCase.Joining: {
|
||||
if (this.props.oobData?.roomType || isSpace) {
|
||||
title = isSpace ? _t("room|joining_space") : _t("room|joining_room");
|
||||
} else {
|
||||
title = _t("room|joining");
|
||||
}
|
||||
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
case MessageCase.Loading: {
|
||||
title = _t("common|loading");
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
case MessageCase.Rejecting: {
|
||||
title = _t("room|rejecting");
|
||||
showSpinner = true;
|
||||
break;
|
||||
}
|
||||
case MessageCase.NotLoggedIn: {
|
||||
const opts: RoomPreviewOpts = { canJoin: false };
|
||||
if (this.props.roomId) {
|
||||
ModuleRunner.instance.invoke(RoomViewLifecycle.PreviewRoomNotLoggedIn, opts, this.props.roomId);
|
||||
}
|
||||
if (opts.canJoin) {
|
||||
title = _t("room|join_title");
|
||||
primaryActionLabel = _t("action|join");
|
||||
primaryActionHandler = (): void => {
|
||||
ModuleRunner.instance.invoke(RoomViewLifecycle.JoinFromRoomPreview, this.props.roomId);
|
||||
};
|
||||
} else {
|
||||
title = _t("room|join_title_account");
|
||||
if (SettingsStore.getValue(UIFeature.Registration)) {
|
||||
primaryActionLabel = _t("room|join_button_account");
|
||||
primaryActionHandler = this.onRegisterClick;
|
||||
}
|
||||
secondaryActionLabel = _t("action|sign_in");
|
||||
secondaryActionHandler = this.onLoginClick;
|
||||
}
|
||||
if (this.props.previewLoading) {
|
||||
footer = (
|
||||
<div>
|
||||
<Spinner w={20} h={20} />
|
||||
{_t("room|loading_preview")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.Kicked: {
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
if (roomName) {
|
||||
title = _t("room|kicked_from_room_by", { memberName, roomName });
|
||||
} else {
|
||||
title = _t("room|kicked_by", { memberName });
|
||||
}
|
||||
subTitle = reason ? _t("room|kick_reason", { reason }) : undefined;
|
||||
|
||||
if (isSpace) {
|
||||
primaryActionLabel = _t("room|forget_space");
|
||||
} else {
|
||||
primaryActionLabel = _t("room|forget_room");
|
||||
}
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
|
||||
if (this.joinRule() !== JoinRule.Invite) {
|
||||
secondaryActionLabel = primaryActionLabel;
|
||||
secondaryActionHandler = primaryActionHandler;
|
||||
|
||||
primaryActionLabel = _t("room|rejoin_button");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.RequestDenied: {
|
||||
title = _t("room|knock_denied_title");
|
||||
|
||||
subTitle = _t("room|knock_denied_subtitle");
|
||||
|
||||
if (isSpace) {
|
||||
primaryActionLabel = _t("room|forget_space");
|
||||
} else {
|
||||
primaryActionLabel = _t("room|forget_room");
|
||||
}
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.Banned: {
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
if (roomName) {
|
||||
title = _t("room|banned_from_room_by", { memberName, roomName });
|
||||
} else {
|
||||
title = _t("room|banned_by", { memberName });
|
||||
}
|
||||
subTitle = reason ? _t("room|kick_reason", { reason }) : undefined;
|
||||
if (isSpace) {
|
||||
primaryActionLabel = _t("room|forget_space");
|
||||
} else {
|
||||
primaryActionLabel = _t("room|forget_room");
|
||||
}
|
||||
primaryActionHandler = this.props.onForgetClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.OtherThreePIDError: {
|
||||
if (roomName) {
|
||||
title = _t("room|3pid_invite_error_title_room", { roomName });
|
||||
} else {
|
||||
title = _t("room|3pid_invite_error_title");
|
||||
}
|
||||
const joinRule = this.joinRule();
|
||||
const errCodeMessage = _t("room|3pid_invite_error_description", {
|
||||
errcode: this.state.threePidFetchError?.errcode || _t("error|unknown_error_code"),
|
||||
});
|
||||
switch (joinRule) {
|
||||
case "invite":
|
||||
subTitle = [_t("room|3pid_invite_error_invite_subtitle"), errCodeMessage];
|
||||
primaryActionLabel = _t("room|3pid_invite_error_invite_action");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
case "public":
|
||||
subTitle = _t("room|3pid_invite_error_public_subtitle");
|
||||
primaryActionLabel = _t("room|join_the_discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
default:
|
||||
subTitle = errCodeMessage;
|
||||
primaryActionLabel = _t("room|3pid_invite_error_invite_action");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailNotFoundInAccount: {
|
||||
if (roomName) {
|
||||
title = _t("room|3pid_invite_email_not_found_account_room", {
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
});
|
||||
} else {
|
||||
title = _t("room|3pid_invite_email_not_found_account", {
|
||||
email: this.props.invitedEmail,
|
||||
});
|
||||
}
|
||||
|
||||
subTitle = _t("room|link_email_to_receive_3pid_invite", { brand });
|
||||
primaryActionLabel = _t("room|join_the_discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailNoIdentityServer: {
|
||||
if (roomName) {
|
||||
title = _t("room|invite_sent_to_email_room", {
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
});
|
||||
} else {
|
||||
title = _t("room|invite_sent_to_email", { email: this.props.invitedEmail });
|
||||
}
|
||||
|
||||
subTitle = _t("room|3pid_invite_no_is_subtitle", {
|
||||
brand,
|
||||
});
|
||||
primaryActionLabel = _t("room|join_the_discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailMismatch: {
|
||||
if (roomName) {
|
||||
title = _t("room|invite_sent_to_email_room", {
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
});
|
||||
} else {
|
||||
title = _t("room|invite_sent_to_email", { email: this.props.invitedEmail });
|
||||
}
|
||||
|
||||
subTitle = _t("room|invite_email_mismatch_suggestion", { brand });
|
||||
primaryActionLabel = _t("room|join_the_discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.Invite: {
|
||||
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
|
||||
|
||||
const inviteMember = this.getInviteMember();
|
||||
let inviterElement: JSX.Element;
|
||||
if (inviteMember) {
|
||||
inviterElement = (
|
||||
<span>
|
||||
<span className="mx_RoomPreviewBar_inviter">{inviteMember.rawDisplayName}</span> (
|
||||
{inviteMember.userId})
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
inviterElement = <span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>;
|
||||
}
|
||||
|
||||
const isDM = this.isDMInvite();
|
||||
if (isDM) {
|
||||
title = _t("room|dm_invite_title", {
|
||||
user: inviteMember?.name ?? this.props.inviterName,
|
||||
});
|
||||
subTitle = [avatar, _t("room|dm_invite_subtitle", {}, { userName: () => inviterElement })];
|
||||
primaryActionLabel = _t("room|dm_invite_action");
|
||||
} else {
|
||||
title = _t("room|invite_title", { roomName });
|
||||
subTitle = [avatar, _t("room|invite_subtitle", {}, { userName: () => inviterElement })];
|
||||
primaryActionLabel = _t("action|accept");
|
||||
}
|
||||
|
||||
const myUserId = MatrixClientPeg.safeGet().getSafeUserId();
|
||||
const member = this.props.room?.currentState.getMember(myUserId);
|
||||
const memberEventContent = member?.events.member?.getContent();
|
||||
|
||||
if (memberEventContent?.reason) {
|
||||
reasonElement = (
|
||||
<InviteReason
|
||||
reason={memberEventContent.reason}
|
||||
htmlReason={memberEventContent[MemberEventHtmlReasonField]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
secondaryActionLabel = _t("action|reject");
|
||||
secondaryActionHandler = this.props.onRejectClick;
|
||||
|
||||
if (this.props.onRejectAndIgnoreClick) {
|
||||
extraComponents.push(
|
||||
<AccessibleButton kind="secondary" onClick={this.props.onRejectAndIgnoreClick} key="ignore">
|
||||
{_t("room|invite_reject_ignore")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageCase.ViewingRoom: {
|
||||
if (this.props.canPreview) {
|
||||
title = _t("room|peek_join_prompt", { roomName });
|
||||
} else if (roomName) {
|
||||
title = _t("room|no_peek_join_prompt", { roomName });
|
||||
} else {
|
||||
title = _t("room|no_peek_no_name_join_prompt");
|
||||
}
|
||||
primaryActionLabel = _t("room|join_the_discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.RoomNotFound: {
|
||||
if (roomName) {
|
||||
title = _t("room|not_found_title_name", { roomName });
|
||||
} else {
|
||||
title = _t("room|not_found_title");
|
||||
}
|
||||
subTitle = _t("room|not_found_subtitle");
|
||||
break;
|
||||
}
|
||||
case MessageCase.OtherError: {
|
||||
if (roomName) {
|
||||
title = _t("room|inaccessible_name", { roomName });
|
||||
} else {
|
||||
title = _t("room|inaccessible");
|
||||
}
|
||||
subTitle = [
|
||||
_t("room|inaccessible_subtitle_1"),
|
||||
_t(
|
||||
"room|inaccessible_subtitle_2",
|
||||
{ errcode: String(this.props.error?.errcode) },
|
||||
{
|
||||
issueLink: (label) => (
|
||||
<a
|
||||
href={SdkConfig.get().feedback.new_issue_url}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
),
|
||||
];
|
||||
break;
|
||||
}
|
||||
case MessageCase.PromptAskToJoin: {
|
||||
if (roomName) {
|
||||
title = _t("room|knock_prompt_name", { roomName });
|
||||
} else {
|
||||
title = _t("room|knock_prompt");
|
||||
}
|
||||
|
||||
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
|
||||
subTitle = [avatar, _t("room|knock_subtitle")];
|
||||
|
||||
reasonElement = (
|
||||
<Field
|
||||
autoFocus
|
||||
className="mx_RoomPreviewBar_fullWidth"
|
||||
element="textarea"
|
||||
onChange={this.onChangeReason}
|
||||
placeholder={_t("room|knock_message_field_placeholder")}
|
||||
type="text"
|
||||
value={this.state.reason ?? ""}
|
||||
/>
|
||||
);
|
||||
|
||||
primaryActionHandler = (): void =>
|
||||
this.props.onSubmitAskToJoin && this.props.onSubmitAskToJoin(this.state.reason);
|
||||
primaryActionLabel = _t("room|knock_send_action");
|
||||
|
||||
break;
|
||||
}
|
||||
case MessageCase.Knocked: {
|
||||
title = _t("room|knock_sent");
|
||||
|
||||
subTitle = [
|
||||
<>
|
||||
<AskToJoinIcon className="mx_Icon mx_Icon_16 mx_RoomPreviewBar_icon" />
|
||||
{_t("room|knock_sent_subtitle")}
|
||||
</>,
|
||||
];
|
||||
|
||||
secondaryActionHandler = this.props.onCancelAskToJoin;
|
||||
secondaryActionLabel = _t("room|knock_cancel_action");
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let subTitleElements;
|
||||
if (subTitle) {
|
||||
if (!Array.isArray(subTitle)) {
|
||||
subTitle = [subTitle];
|
||||
}
|
||||
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{t}</p>);
|
||||
}
|
||||
|
||||
let titleElement;
|
||||
if (showSpinner) {
|
||||
titleElement = (
|
||||
<h3 className="mx_RoomPreviewBar_spinnerTitle">
|
||||
<Spinner />
|
||||
{title}
|
||||
</h3>
|
||||
);
|
||||
} else {
|
||||
titleElement = <h3>{title}</h3>;
|
||||
}
|
||||
|
||||
let primaryButton;
|
||||
if (primaryActionHandler) {
|
||||
primaryButton = (
|
||||
<AccessibleButton kind="primary" onClick={primaryActionHandler}>
|
||||
{primaryActionLabel}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
let secondaryButton;
|
||||
if (secondaryActionHandler) {
|
||||
secondaryButton = (
|
||||
<AccessibleButton kind="secondary" onClick={secondaryActionHandler}>
|
||||
{secondaryActionLabel}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCommunityRoom) {
|
||||
secondaryButton = primaryButton;
|
||||
primaryButton = <MessageCommunityBotButton text="Message Superhero Bot to get tokens" />;
|
||||
titleElement = <CommunityRoomPeekMessage roomName={roomName} />;
|
||||
}
|
||||
|
||||
const isPanel = this.props.canPreview;
|
||||
|
||||
const classes = classNames("mx_RoomPreviewBar", `mx_RoomPreviewBar_${messageCase}`, {
|
||||
mx_RoomPreviewBar_panel: isPanel,
|
||||
mx_RoomPreviewBar_dialog: !isPanel,
|
||||
});
|
||||
|
||||
// ensure correct tab order for both views
|
||||
const actions = isPanel ? (
|
||||
<>
|
||||
{secondaryButton}
|
||||
{extraComponents}
|
||||
{primaryButton}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{primaryButton}
|
||||
{extraComponents}
|
||||
{secondaryButton}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div role="complementary" className={classes}>
|
||||
<div className="mx_RoomPreviewBar_message">
|
||||
{titleElement}
|
||||
{subTitleElements}
|
||||
</div>
|
||||
{reasonElement}
|
||||
<div
|
||||
className={classNames("mx_RoomPreviewBar_actions", {
|
||||
mx_RoomPreviewBar_fullWidth: messageCase === MessageCase.PromptAskToJoin,
|
||||
})}
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_footer">{footer}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,19 +1,60 @@
|
|||
import { useAtom } from "jotai";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
|
||||
import { verifiedAccountsAtom } from "../atoms";
|
||||
import { communityBotAtom, minimumTokenThresholdAtom, verifiedAccountsAtom } from "../atoms";
|
||||
|
||||
const useMinimumTokenThreshold = (config: any): void => {
|
||||
const [, setMinimumTokenThreshold] = useAtom(minimumTokenThresholdAtom);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const loadMinimumTokenThreshold = useCallback(() => {
|
||||
if (config.bots_backend_url && !isLoading) {
|
||||
setIsLoading(true);
|
||||
fetch(`${config.bots_backend_url}/ui/minimum-token-threshold`, {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(setMinimumTokenThreshold)
|
||||
.catch((e) => {
|
||||
console.error("Error loading minimum token threshold", e);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [config.bots_backend_url, isLoading, setMinimumTokenThreshold]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMinimumTokenThreshold();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
loadMinimumTokenThreshold();
|
||||
}, 10000);
|
||||
|
||||
return (): void => clearInterval(interval);
|
||||
}, [loadMinimumTokenThreshold]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides the superhero context to its children components.
|
||||
* @param children The child components to be wrapped by the provider.
|
||||
* @param config The SDK config
|
||||
* @returns The superhero provider component.
|
||||
*/
|
||||
export const SuperheroProvider = ({ children, config }: any): any => {
|
||||
const [verifiedAccounts, setVerifiedAccounts] = useAtom(verifiedAccountsAtom);
|
||||
const [, setCommunityBot] = useAtom(communityBotAtom);
|
||||
|
||||
useEffect(() => {
|
||||
setCommunityBot({
|
||||
userId: config.community_bot_user_id,
|
||||
rawDisplayName: "Community DAO Room Bot",
|
||||
});
|
||||
}, [setCommunityBot, config.community_bot_user_id]);
|
||||
|
||||
function loadVerifiedAccounts(): void {
|
||||
if (config.bots_backend_url) {
|
||||
fetch(`${config.bots_backend_url}/ae-wallet-bot/get-verified-accounts`, {
|
||||
fetch(`${config.bots_backend_url}/ui/get-verified-accounts`, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
@ -37,5 +78,8 @@ export const SuperheroProvider = ({ children, config }: any): any => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Load minimum token threshold
|
||||
useMinimumTokenThreshold(config);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
|
|
@ -22,3 +22,26 @@ export function useVerifiedRoom(room?: Room | IPublicRoomsChunkRoom): {
|
|||
isCommunityRoom,
|
||||
};
|
||||
}
|
||||
|
||||
export const cleanRoomName = (roomName: string): string => {
|
||||
// remove # in the beginning
|
||||
let parsedName = roomName.startsWith("#") ? roomName.slice(1) : roomName;
|
||||
|
||||
// remove domain
|
||||
parsedName = parsedName.split(":")[0];
|
||||
|
||||
return parsedName;
|
||||
};
|
||||
|
||||
export const isVerifiedRoom = (
|
||||
roomName: string,
|
||||
): {
|
||||
isTokenGatedRoom: boolean;
|
||||
isCommunityRoom: boolean;
|
||||
} => {
|
||||
const parsedRoomName = cleanRoomName(roomName);
|
||||
return {
|
||||
isTokenGatedRoom: parsedRoomName.startsWith("[TG]"),
|
||||
isCommunityRoom: parsedRoomName.startsWith("$"),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -40,6 +40,10 @@
|
|||
"superhero_dex": "Superhero DEX",
|
||||
"mint_a_token": "Mint a token",
|
||||
"composer": {
|
||||
"no_perms_notice": "You need to own the room token to be able to post in this room."
|
||||
"no_perms_token_notice": "You need to own more than %(threshold)s %(symbol)s tokens to be able to post in this room."
|
||||
},
|
||||
"room": {
|
||||
"no_peek_join_prompt_community": "%(roomName)s is a private token-gated room.",
|
||||
"no_peek_join_prompt_community_threshold": "You need to own %(threshold)s %(symbol)s tokens to access it."
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue