use ExternalLink components for external links (#10758)

* use ExternalLink components for external links

* test

* strict
This commit is contained in:
Kerry 2023-05-04 09:26:26 +12:00 committed by GitHub
parent 42e6c9839c
commit 37b7dfe943
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 281 additions and 47 deletions

View file

@ -30,6 +30,7 @@ import AccessibleButton from "../views/elements/AccessibleButton";
import InlineSpinner from "../views/elements/InlineSpinner"; import InlineSpinner from "../views/elements/InlineSpinner";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages"; import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
import ExternalLink from "../views/elements/ExternalLink";
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
@ -213,9 +214,9 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
{}, {},
{ {
consentLink: (sub) => ( consentLink: (sub) => (
<a href={consentError!.data?.consent_uri} target="_blank" rel="noreferrer noopener"> <ExternalLink href={consentError!.data?.consent_uri} target="_blank" rel="noreferrer noopener">
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
); );

View file

@ -77,6 +77,7 @@ import MainSplit from "./MainSplit";
import RightPanel from "./RightPanel"; import RightPanel from "./RightPanel";
import SpaceHierarchy, { showRoom } from "./SpaceHierarchy"; import SpaceHierarchy, { showRoom } from "./SpaceHierarchy";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import ExternalLink from "../views/elements/ExternalLink";
interface IProps { interface IProps {
space: Room; space: Room;
@ -593,9 +594,9 @@ const SpaceSetupPrivateInvite: React.FC<{
{ {
b: (sub) => <b>{sub}</b>, b: (sub) => <b>{sub}</b>,
link: () => ( link: () => (
<a href="https://app.element.io/" rel="noreferrer noopener" target="_blank"> <ExternalLink href="https://app.element.io/" rel="noreferrer noopener" target="_blank">
app.element.io app.element.io
</a> </ExternalLink>
), ),
}, },
)} )}

View file

@ -22,6 +22,7 @@ import DialogButtons from "../elements/DialogButtons";
import Modal, { ComponentProps } from "../../../Modal"; import Modal, { ComponentProps } from "../../../Modal";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import { getPolicyUrl } from "../../../toasts/AnalyticsToast"; import { getPolicyUrl } from "../../../toasts/AnalyticsToast";
import ExternalLink from "../elements/ExternalLink";
export enum ButtonClicked { export enum ButtonClicked {
Primary, Primary,
@ -55,10 +56,10 @@ export const AnalyticsLearnMoreDialog: React.FC<IProps> = ({
{ {
PrivacyPolicyUrl: (sub) => { PrivacyPolicyUrl: (sub) => {
return ( return (
<a href={privacyPolicyUrl} rel="norefferer noopener" target="_blank"> <ExternalLink href={privacyPolicyUrl} rel="norefferer noopener" target="_blank">
{sub} {sub}
<span className="mx_AnalyticsPolicyLink" /> <span className="mx_AnalyticsPolicyLink" />
</a> </ExternalLink>
); );
}, },
}, },

View file

@ -27,6 +27,7 @@ import InfoDialog from "./InfoDialog";
import { submitFeedback } from "../../../rageshake/submit-rageshake"; import { submitFeedback } from "../../../rageshake/submit-rageshake";
import { useStateToggle } from "../../../hooks/useStateToggle"; import { useStateToggle } from "../../../hooks/useStateToggle";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import ExternalLink from "../elements/ExternalLink";
interface IProps { interface IProps {
feature?: string; feature?: string;
@ -130,16 +131,20 @@ const FeedbackDialog: React.FC<IProps> = (props: IProps) => {
{ {
existingIssuesLink: (sub) => { existingIssuesLink: (sub) => {
return ( return (
<a target="_blank" rel="noreferrer noopener" href={existingIssuesUrl}> <ExternalLink
target="_blank"
rel="noreferrer noopener"
href={existingIssuesUrl}
>
{sub} {sub}
</a> </ExternalLink>
); );
}, },
newIssueLink: (sub) => { newIssueLink: (sub) => {
return ( return (
<a target="_blank" rel="noreferrer noopener" href={newIssueUrl}> <ExternalLink target="_blank" rel="noreferrer noopener" href={newIssueUrl}>
{sub} {sub}
</a> </ExternalLink>
); );
}, },
}, },

View file

@ -28,6 +28,7 @@ import StyledRadioButton from "../elements/StyledRadioButton";
import TextWithTooltip from "../elements/TextWithTooltip"; import TextWithTooltip from "../elements/TextWithTooltip";
import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation";
import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig";
import ExternalLink from "../elements/ExternalLink";
interface IProps { interface IProps {
title?: string; title?: string;
@ -236,9 +237,13 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
</AccessibleButton> </AccessibleButton>
<h2>{_t("Learn more")}</h2> <h2>{_t("Learn more")}</h2>
<a href="https://matrix.org/faq/#what-is-a-homeserver%3F" target="_blank" rel="noreferrer noopener"> <ExternalLink
href="https://matrix.org/faq/#what-is-a-homeserver%3F"
target="_blank"
rel="noreferrer noopener"
>
{_t("About homeservers")} {_t("About homeservers")}
</a> </ExternalLink>
</form> </form>
</BaseDialog> </BaseDialog>
); );

View file

@ -22,6 +22,7 @@ import { _t, pickBestLanguage } from "../../../languageHandler";
import DialogButtons from "../elements/DialogButtons"; import DialogButtons from "../elements/DialogButtons";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import { ServicePolicyPair } from "../../../Terms"; import { ServicePolicyPair } from "../../../Terms";
import ExternalLink from "../elements/ExternalLink";
interface ITermsCheckboxProps { interface ITermsCheckboxProps {
onChange: (url: string, checked: boolean) => void; onChange: (url: string, checked: boolean) => void;
@ -148,9 +149,9 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
<td className="mx_TermsDialog_summary">{summary}</td> <td className="mx_TermsDialog_summary">{summary}</td>
<td> <td>
{termDoc[termsLang].name} {termDoc[termsLang].name}
<a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}> <ExternalLink rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
<span className="mx_TermsDialog_link" /> <span className="mx_TermsDialog_link" />
</a> </ExternalLink>
</td> </td>
<td> <td>
<TermsCheckbox <TermsCheckbox

View file

@ -26,6 +26,7 @@ import EventIndexPeg from "../../../indexing/EventIndexPeg";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import SeshatResetDialog from "../dialogs/SeshatResetDialog"; import SeshatResetDialog from "../dialogs/SeshatResetDialog";
import InlineSpinner from "../elements/InlineSpinner"; import InlineSpinner from "../elements/InlineSpinner";
import ExternalLink from "../elements/ExternalLink";
interface IState { interface IState {
enabling: boolean; enabling: boolean;
@ -197,9 +198,9 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
}, },
{ {
nativeLink: (sub) => ( nativeLink: (sub) => (
<a href={nativeLink} target="_blank" rel="noreferrer noopener"> <ExternalLink href={nativeLink} target="_blank" rel="noreferrer noopener">
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
)} )}
@ -217,9 +218,13 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
}, },
{ {
desktopLink: (sub) => ( desktopLink: (sub) => (
<a href="https://element.io/get-started" target="_blank" rel="noreferrer noopener"> <ExternalLink
href="https://element.io/get-started"
target="_blank"
rel="noreferrer noopener"
>
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
)} )}

View file

@ -31,6 +31,7 @@ import { Action } from "../../../../../dispatcher/actions";
import { UserTab } from "../../../dialogs/UserTab"; import { UserTab } from "../../../dialogs/UserTab";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import CopyableText from "../../../elements/CopyableText"; import CopyableText from "../../../elements/CopyableText";
import ExternalLink from "../../../elements/ExternalLink";
interface IProps { interface IProps {
closeSettingsFn: () => void; closeSettingsFn: () => void;
@ -114,9 +115,9 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
for (const tocEntry of tocLinks) { for (const tocEntry of tocLinks) {
legalLinks.push( legalLinks.push(
<div key={tocEntry.url}> <div key={tocEntry.url}>
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank"> <ExternalLink href={tocEntry.url} rel="noreferrer noopener" target="_blank">
{tocEntry.text} {tocEntry.text}
</a> </ExternalLink>
</div>, </div>,
); );
} }
@ -143,27 +144,31 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
{}, {},
{ {
photo: (sub) => ( photo: (sub) => (
<a <ExternalLink
href="themes/element/img/backgrounds/lake.jpg" href="themes/element/img/backgrounds/lake.jpg"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
{sub} {sub}
</a> </ExternalLink>
), ),
author: (sub) => ( author: (sub) => (
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank"> <ExternalLink
href="https://www.flickr.com/golan"
rel="noreferrer noopener"
target="_blank"
>
{sub} {sub}
</a> </ExternalLink>
), ),
terms: (sub) => ( terms: (sub) => (
<a <ExternalLink
href="https://creativecommons.org/licenses/by-sa/4.0/" href="https://creativecommons.org/licenses/by-sa/4.0/"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
)} )}
@ -175,27 +180,27 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
{}, {},
{ {
colr: (sub) => ( colr: (sub) => (
<a <ExternalLink
href="https://github.com/matrix-org/twemoji-colr" href="https://github.com/matrix-org/twemoji-colr"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
{sub} {sub}
</a> </ExternalLink>
), ),
author: (sub) => ( author: (sub) => (
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank"> <ExternalLink href="https://mozilla.org" rel="noreferrer noopener" target="_blank">
{sub} {sub}
</a> </ExternalLink>
), ),
terms: (sub) => ( terms: (sub) => (
<a <ExternalLink
href="https://www.apache.org/licenses/LICENSE-2.0" href="https://www.apache.org/licenses/LICENSE-2.0"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
)} )}
@ -208,23 +213,31 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
{}, {},
{ {
twemoji: (sub) => ( twemoji: (sub) => (
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank"> <ExternalLink
href="https://twemoji.twitter.com/"
rel="noreferrer noopener"
target="_blank"
>
{sub} {sub}
</a> </ExternalLink>
), ),
author: (sub) => ( author: (sub) => (
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank"> <ExternalLink
href="https://twemoji.twitter.com/"
rel="noreferrer noopener"
target="_blank"
>
{sub} {sub}
</a> </ExternalLink>
), ),
terms: (sub) => ( terms: (sub) => (
<a <ExternalLink
href="https://creativecommons.org/licenses/by/4.0/" href="https://creativecommons.org/licenses/by/4.0/"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
)} )}
@ -256,9 +269,9 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
}, },
{ {
a: (sub) => ( a: (sub) => (
<a href="https://element.io/help" rel="noreferrer noopener" target="_blank"> <ExternalLink href="https://element.io/help" rel="noreferrer noopener" target="_blank">
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
); );
@ -273,9 +286,9 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
}, },
{ {
a: (sub) => ( a: (sub) => (
<a href="https://element.io/help" rel="noreferrer noopener" target="_blank"> <ExternalLink href="https://element.io/help" rel="noreferrer noopener" target="_blank">
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
)} )}
@ -321,13 +334,13 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
{}, {},
{ {
a: (sub) => ( a: (sub) => (
<a <ExternalLink
href="https://matrix.org/security-disclosure-policy/" href="https://matrix.org/security-disclosure-policy/"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
)} )}

View file

@ -20,6 +20,7 @@ import { MatrixError, ConnectionError } from "matrix-js-sdk/src/http-api";
import { _t, _td, Tags, TranslatedString } from "../languageHandler"; import { _t, _td, Tags, TranslatedString } from "../languageHandler";
import SdkConfig from "../SdkConfig"; import SdkConfig from "../SdkConfig";
import { ValidatedServerConfig } from "./ValidatedServerConfig"; import { ValidatedServerConfig } from "./ValidatedServerConfig";
import ExternalLink from "../components/views/elements/ExternalLink";
export const resourceLimitStrings = { export const resourceLimitStrings = {
"monthly_active_user": _td("This homeserver has hit its Monthly Active User limit."), "monthly_active_user": _td("This homeserver has hit its Monthly Active User limit."),
@ -183,9 +184,9 @@ export function messageForConnectionError(
{}, {},
{ {
a: (sub) => ( a: (sub) => (
<a target="_blank" rel="noreferrer noopener" href={serverConfig.hsUrl}> <ExternalLink target="_blank" rel="noreferrer noopener" href={serverConfig.hsUrl}>
{sub} {sub}
</a> </ExternalLink>
), ),
}, },
)} )}

View file

@ -14,11 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react";
import { render } from "@testing-library/react";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client"; import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { getUnsentMessages } from "../../../src/components/structures/RoomStatusBar"; import RoomStatusBar, { getUnsentMessages } from "../../../src/components/structures/RoomStatusBar";
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg"; import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import { mkEvent, stubClient } from "../../test-utils/test-utils"; import { mkEvent, stubClient } from "../../test-utils/test-utils";
import { mkThread } from "../../test-utils/threads"; import { mkThread } from "../../test-utils/threads";
@ -34,6 +38,7 @@ describe("RoomStatusBar", () => {
stubClient(); stubClient();
client = MatrixClientPeg.get(); client = MatrixClientPeg.get();
client.getSyncStateData = jest.fn().mockReturnValue({});
room = new Room(ROOM_ID, client, client.getUserId()!, { room = new Room(ROOM_ID, client, client.getUserId()!, {
pendingEventOrdering: PendingEventOrdering.Detached, pendingEventOrdering: PendingEventOrdering.Detached,
}); });
@ -47,6 +52,13 @@ describe("RoomStatusBar", () => {
event.status = EventStatus.NOT_SENT; event.status = EventStatus.NOT_SENT;
}); });
const getComponent = () =>
render(<RoomStatusBar room={room} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
),
});
describe("getUnsentMessages", () => { describe("getUnsentMessages", () => {
it("returns no unsent messages", () => { it("returns no unsent messages", () => {
expect(getUnsentMessages(room)).toHaveLength(0); expect(getUnsentMessages(room)).toHaveLength(0);
@ -88,4 +100,55 @@ describe("RoomStatusBar", () => {
expect(pendingEvents.every((ev) => ev.getId() !== event.getId())).toBe(true); expect(pendingEvents.every((ev) => ev.getId() !== event.getId())).toBe(true);
}); });
}); });
describe("<RoomStatusBar />", () => {
it("should render nothing when room has no error or unsent messages", () => {
const { container } = getComponent();
expect(container.firstChild).toBe(null);
});
describe("unsent messages", () => {
it("should render warning when messages are unsent due to consent", () => {
const unsentMessage = mkEvent({
event: true,
type: "m.room.message",
user: "@user1:server",
room: "!room1:server",
content: {},
});
unsentMessage.status = EventStatus.NOT_SENT;
unsentMessage.error = new MatrixError({
errcode: "M_CONSENT_NOT_GIVEN",
data: { consent_uri: "terms.com" },
});
room.addPendingEvent(unsentMessage, "123");
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
it("should render warning when messages are unsent due to resource limit", () => {
const unsentMessage = mkEvent({
event: true,
type: "m.room.message",
user: "@user1:server",
room: "!room1:server",
content: {},
});
unsentMessage.status = EventStatus.NOT_SENT;
unsentMessage.error = new MatrixError({
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
data: { limit_type: "monthly_active_user" },
});
room.addPendingEvent(unsentMessage, "123");
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
});
});
}); });

View file

@ -0,0 +1,126 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RoomStatusBar <RoomStatusBar /> unsent messages should render warning when messages are unsent due to consent 1`] = `
<div>
<div
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
>
<div
role="alert"
>
<div
class="mx_RoomStatusBar_unsentBadge"
>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char"
>
<span
class="mx_NotificationBadge_count"
>
!
</span>
</div>
</div>
<div>
<div
class="mx_RoomStatusBar_unsentTitle"
>
<span>
You can't send any messages until you review and agree to
<a
class="mx_ExternalLink"
rel="noreferrer noopener"
target="_blank"
>
our terms and conditions
<i
class="mx_ExternalLink_icon"
/>
</a>
.
</span>
</div>
<div
class="mx_RoomStatusBar_unsentDescription"
>
You can select all or individual messages to retry or delete
</div>
</div>
<div
class="mx_RoomStatusBar_unsentButtonBar"
>
<div
class="mx_AccessibleButton mx_RoomStatusBar_unsentCancelAllBtn"
role="button"
tabindex="0"
>
Delete all
</div>
<div
class="mx_AccessibleButton mx_RoomStatusBar_unsentRetry"
role="button"
tabindex="0"
>
Retry all
</div>
</div>
</div>
</div>
</div>
`;
exports[`RoomStatusBar <RoomStatusBar /> unsent messages should render warning when messages are unsent due to resource limit 1`] = `
<div>
<div
class="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages"
>
<div
role="alert"
>
<div
class="mx_RoomStatusBar_unsentBadge"
>
<div
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char"
>
<span
class="mx_NotificationBadge_count"
>
!
</span>
</div>
</div>
<div>
<div
class="mx_RoomStatusBar_unsentTitle"
>
Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.
</div>
<div
class="mx_RoomStatusBar_unsentDescription"
>
You can select all or individual messages to retry or delete
</div>
</div>
<div
class="mx_RoomStatusBar_unsentButtonBar"
>
<div
class="mx_AccessibleButton mx_RoomStatusBar_unsentCancelAllBtn"
role="button"
tabindex="0"
>
Delete all
</div>
<div
class="mx_AccessibleButton mx_RoomStatusBar_unsentRetry"
role="button"
tabindex="0"
>
Retry all
</div>
</div>
</div>
</div>
</div>
`;

View file

@ -38,19 +38,27 @@ exports[`FeedbackDialog should respect feedback config 1`] = `
<span> <span>
Please view Please view
<a <a
class="mx_ExternalLink"
href="http://existing?foo=bar" href="http://existing?foo=bar"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
existing bugs on Github existing bugs on Github
<i
class="mx_ExternalLink_icon"
/>
</a> </a>
first. No match? first. No match?
<a <a
class="mx_ExternalLink"
href="https://new.issue.url?foo=bar" href="https://new.issue.url?foo=bar"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
Start a new one Start a new one
<i
class="mx_ExternalLink_icon"
/>
</a> </a>
. .
</span> </span>

View file

@ -6,11 +6,15 @@ exports[`messageForConnectionError should match snapshot for ConnectionError 1`]
<span> <span>
Can't connect to homeserver - please check your connectivity, ensure your Can't connect to homeserver - please check your connectivity, ensure your
<a <a
class="mx_ExternalLink"
href="hsUrl" href="hsUrl"
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
homeserver's SSL certificate homeserver's SSL certificate
<i
class="mx_ExternalLink_icon"
/>
</a> </a>
is trusted, and that a browser extension is not blocking requests. is trusted, and that a browser extension is not blocking requests.
</span> </span>