diff --git a/components.json b/components.json index bbf8fea8a9..3c34db1d97 100644 --- a/components.json +++ b/components.json @@ -2,9 +2,10 @@ "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/RoomTile.tsx": "src/components/views/rooms/RoomTile.tsx", - "src/components/views/rooms/NewRoomIntro.tsx": "src/components/views/rooms/NewRoomIntro.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", "src/components/views/avatars/BaseAvatar.tsx": "src/components/views/avatars/BaseAvatar.tsx", diff --git a/src/components/views/elements/CommunityRoomPeekMessage.tsx b/src/components/views/elements/CommunityRoomPeekMessage.tsx new file mode 100644 index 0000000000..c8664590e9 --- /dev/null +++ b/src/components/views/elements/CommunityRoomPeekMessage.tsx @@ -0,0 +1,18 @@ +import { useAtom } from "jotai"; +import { minimumTokenThresholdAtom } from "../../../atoms"; +import { _t } from "../../../languageHandler"; +import React, { ReactElement } from "react"; +import { cleanRoomName } from "../../../hooks/useVerifiedRoom"; + +export function CommunityRoomPeekMessage({ roomName }: { roomName: string }): ReactElement { + const [allTokens] = useAtom(minimumTokenThresholdAtom) + const cleanedRoomName = cleanRoomName(roomName); + + let tokenThreshold = allTokens[cleanedRoomName]; + + return ( +

{_t("room|no_peek_join_prompt_community", { roomName: cleanedRoomName })} { + tokenThreshold ? (_t('room|no_peek_join_prompt_community_threshold', tokenThreshold)) : '' + }

+ ); +} diff --git a/src/components/views/elements/DisabledMessageField.tsx b/src/components/views/elements/DisabledMessageField.tsx index 73ea565472..6c29bdabed 100644 --- a/src/components/views/elements/DisabledMessageField.tsx +++ b/src/components/views/elements/DisabledMessageField.tsx @@ -1,14 +1,13 @@ import { useAtom } from "jotai"; -import { communityBotAtom, minimumTokenThresholdAtom } from "../../../atoms"; +import { minimumTokenThresholdAtom } from "../../../atoms"; import { _t } from "../../../languageHandler"; import React from "react"; import { useVerifiedRoom } from "../../../hooks/useVerifiedRoom"; import { Room } from "matrix-js-sdk/src/matrix"; -import { MessageButton } from "./MessageButton"; +import { MessageCommunityBotButton } from "./MessageButton"; export function DisabledMessageField({ room }: { room: Room }): JSX.Element { const [allTokens] = useAtom(minimumTokenThresholdAtom) - const [communityBot] = useAtom(communityBotAtom) const { isTokenGatedRoom, isCommunityRoom, } = useVerifiedRoom(room); let tokenThreshold = allTokens[room.name]; @@ -26,11 +25,11 @@ export function DisabledMessageField({ room }: { room: Room }): JSX.Element { if (tokenThreshold) { return (
- You need at least {tokenThreshold.threshold} {tokenThreshold.symbol} to join this - community.{ isCommunityRoom ? ( + {_t("composer|no_perms_token_notice", tokenThreshold)} + { isCommunityRoom ? ( <> - + ) : null }
diff --git a/src/components/views/elements/MessageButton.tsx b/src/components/views/elements/MessageButton.tsx index 15ba43997b..1981342aee 100644 --- a/src/components/views/elements/MessageButton.tsx +++ b/src/components/views/elements/MessageButton.tsx @@ -6,7 +6,8 @@ import { Icon as SendMessage } from "../../../../res/themes/superhero/img/icons/ import { MatrixClient, RoomMember, User } from "matrix-js-sdk/src/matrix"; import { DirectoryMember, startDmOnFirstMessage } from "matrix-react-sdk/src/utils/direct-messages"; -import { BareUser } from "../../../atoms"; +import { BareUser, communityBotAtom } from "../../../atoms"; +import { useAtom } from "jotai"; /** * Converts the member to a DirectoryMember and starts a DM with them. */ @@ -41,3 +42,13 @@ export const MessageButton = ({ member, text = 'Send Message' }: { member: Membe ); }; + +export const MessageCommunityBotButton = ({ text = 'Send Message' }: { text?: string }): JSX.Element => { + const [communityBot] = useAtom(communityBotAtom) + + return ( + + ); +}; + + diff --git a/src/components/views/rooms/RoomPreviewBar.tsx b/src/components/views/rooms/RoomPreviewBar.tsx new file mode 100644 index 0000000000..263d92b53b --- /dev/null +++ b/src/components/views/rooms/RoomPreviewBar.tsx @@ -0,0 +1,753 @@ +/* +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 { _t } from "../../../languageHandler"; +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 { 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 { + public static defaultProps = { + onJoinClick() {}, + }; + + 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 { + // 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().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 } { + 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): 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 = () => { + 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 = ( +
+ + {_t("room|loading_preview")} +
+ ); + } + 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 = ; + + const inviteMember = this.getInviteMember(); + let inviterElement: JSX.Element; + if (inviteMember) { + inviterElement = ( + + {inviteMember.rawDisplayName} ( + {inviteMember.userId}) + + ); + } else { + inviterElement = {this.props.inviterName}; + } + + 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 = ( + + ); + } + + primaryActionHandler = this.props.onJoinClick; + secondaryActionLabel = _t("action|reject"); + secondaryActionHandler = this.props.onRejectClick; + + if (this.props.onRejectAndIgnoreClick) { + extraComponents.push( + + {_t("room|invite_reject_ignore")} + , + ); + } + 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) => ( + + {label} + + ), + }, + ), + ]; + break; + } + case MessageCase.PromptAskToJoin: { + if (roomName) { + title = _t("room|knock_prompt_name", { roomName }); + } else { + title = _t("room|knock_prompt"); + } + + const avatar = ; + subTitle = [avatar, _t("room|knock_subtitle")]; + + reasonElement = ( + + ); + + primaryActionHandler = () => + 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 = [ + <> + + {_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) =>

{t}

); + } + + let titleElement; + if (showSpinner) { + titleElement = ( +

+ + {title} +

+ ); + } else { + titleElement =

{title}

; + } + + let primaryButton; + if (primaryActionHandler) { + primaryButton = ( + + {primaryActionLabel} + + ); + } + + let secondaryButton; + if (secondaryActionHandler) { + secondaryButton = ( + + {secondaryActionLabel} + + ); + } + + if(isCommunityRoom) { + secondaryButton = primaryButton; + primaryButton = (); + titleElement = (); + } + + 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 ( +
+
+ {titleElement} + {subTitleElements} +
+ {reasonElement} +
+ {actions} +
+
{footer}
+
+ ); + } +} diff --git a/src/hooks/useVerifiedRoom.ts b/src/hooks/useVerifiedRoom.ts index 9b9cffe7de..3e0e603421 100644 --- a/src/hooks/useVerifiedRoom.ts +++ b/src/hooks/useVerifiedRoom.ts @@ -22,3 +22,21 @@ export function useVerifiedRoom(room?: Room | IPublicRoomsChunkRoom): { isCommunityRoom, }; } + +export function cleanRoomName(roomName: string) { + // remove # in the beginning + let parsedName = roomName.startsWith('#') ? roomName.slice(1) : roomName; + + // remove domain + parsedName = parsedName.split(':')[0]; + + return parsedName; +} + +export function isVerifiedRoom(roomName: string) { + let parsedRoomName = cleanRoomName(roomName); + return { + isTokenGatedRoom: parsedRoomName.startsWith("[TG]"), + isCommunityRoom: parsedRoomName.startsWith("$"), + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 27588f25bf..d495c1b6d0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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." } }