Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions src/components/AnnotationDocumentInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { Link } from '@hypothesis/frontend-shared';

import type { Annotation } from '../helpers/annotation-metadata';
import { domainAndTitle } from '../helpers/annotation-metadata';

export type AnnotationDocumentInfoProps = {
/** The domain associated with the document */
domain?: string;
/** A direct link to the document */
link?: string;
title: string;
annotation: Annotation;
};

/**
* Render some metadata about an annotation's document and link to it
* if a link is available.
*/
export default function AnnotationDocumentInfo({
domain,
link,
title,
annotation,
}: AnnotationDocumentInfoProps) {
const { domain, titleText: title, titleLink } = domainAndTitle(annotation);
const annotationURL = annotation.links?.html || '';
// There are some cases at present in which linking directly to an
// annotation's document is not immediately feasible—e.g in an LMS context
// where the original document might not be available outside of an
// assignment (e.g. Canvas files), and/or wouldn't be able to present
// any associated annotations.
// For the present, disable links to annotation documents for all third-party
// annotations until we have a more nuanced way of making linking determinations.
// The absence of a link to a single-annotation view is a signal that this
// is a third-party annotation.
// Also, of course, verify that there is a URL to the document (titleLink)
const link = annotationURL && titleLink ? titleLink : '';

return (
<div className="flex gap-x-1">
<div className="text-color-text-light">
Expand Down
46 changes: 31 additions & 15 deletions src/components/test/AnnotationDocumentInfo-test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import { checkAccessibility } from '@hypothesis/frontend-testing';
import {
checkAccessibility,
mockImportedComponents,
} from '@hypothesis/frontend-testing';
import { mount } from '@hypothesis/frontend-testing';

import AnnotationDocumentInfo from '../AnnotationDocumentInfo';
import AnnotationDocumentInfo, { $imports } from '../AnnotationDocumentInfo';

describe('AnnotationDocumentInfo', () => {
const createAnnotationDocumentInfo = props => {
return mount(
<AnnotationDocumentInfo
domain="www.foo.bar"
link="http://www.baz"
title="Turtles"
{...props}
/>,
);
let fakeAnnotation;
let fakeDomainAndTitle;

beforeEach(() => {
fakeAnnotation = {
links: { html: 'https://example.com' },
};
fakeDomainAndTitle = sinon.stub().returns({
titleText: 'Turtles',
titleLink: 'http://www.baz',
domain: 'www.foo.com',
});

$imports.$mock(mockImportedComponents());
$imports.$mock({
'../helpers/annotation-metadata': {
domainAndTitle: fakeDomainAndTitle,
},
});
});

const createAnnotationDocumentInfo = () => {
return mount(<AnnotationDocumentInfo annotation={fakeAnnotation} />);
};

it('should render the document title', () => {
Expand All @@ -32,7 +49,8 @@ describe('AnnotationDocumentInfo', () => {
it('does not link to document when no link available', () => {});

it('should render domain if available', () => {
const wrapper = createAnnotationDocumentInfo({ link: '' });
fakeAnnotation.links.html = '';
const wrapper = createAnnotationDocumentInfo();

const link = wrapper.find('a');
assert.include(wrapper.text(), '"Turtles"');
Expand All @@ -42,9 +60,7 @@ describe('AnnotationDocumentInfo', () => {
it(
'should pass a11y checks',
checkAccessibility({
content: () => {
return createAnnotationDocumentInfo();
},
content: () => createAnnotationDocumentInfo(),
}),
);
});
115 changes: 115 additions & 0 deletions src/helpers/annotation-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
export type Annotation = {
uri: string;
document: { title: string };
links: {
/** A "bouncer" URL that takes the user to see the annotation in context */
incontext?: string;
/** URL to view the annotation by itself. */
html?: string;
};
};
Comment on lines +1 to +10
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have created this type as small as possible, just with the minimum properties that are needed by the helpers defined here.

In future we plan to extract the type definitions to a shared location.


export type DocumentMetadata = {
uri: string;
domain: string;
title: string;
};

/**
* Extract document metadata from an annotation.
*/
export function documentMetadata(annotation: Annotation): DocumentMetadata {
const uri = annotation.uri;

let domain;
try {
domain = new URL(uri).hostname;
} catch {
// Annotation URI parsing on the backend is very liberal compared to the URL
// constructor. There is also some historic invalid data in h (eg [1]).
// Hence, we must handle URL parsing failures in the client.
//
// [1] https://github.com/hypothesis/client/issues/3666
domain = '';
}
if (domain === 'localhost') {
domain = '';
}

let title = domain;
if (annotation.document && annotation.document.title) {
title = annotation.document.title[0];
}

return { uri, domain, title };
}

export type DomainAndTitle = {
domain: string;
titleText: string;
titleLink: string | null;
};

/**
* Return the domain and title of an annotation for display on an annotation
* card.
*/
export function domainAndTitle(annotation: Annotation): DomainAndTitle {
return {
domain: domainTextFromAnnotation(annotation),
titleText: titleTextFromAnnotation(annotation),
titleLink: titleLinkFromAnnotation(annotation),
};
}

function titleLinkFromAnnotation(annotation: Annotation): string | null {
let titleLink: string | null = annotation.uri;

if (
titleLink &&
!(titleLink.indexOf('http://') === 0 || titleLink.indexOf('https://') === 0)
) {
// We only link to http(s) URLs.
titleLink = null;
}

if (annotation.links && annotation.links.incontext) {
titleLink = annotation.links.incontext;
}

return titleLink;
}
/**
* Returns the domain text from an annotation.
*/
function domainTextFromAnnotation(annotation: Annotation): string {
const document = documentMetadata(annotation);

let domainText = '';
if (document.uri && document.uri.indexOf('file://') === 0 && document.title) {
const parts = document.uri.split('/');
const filename = parts[parts.length - 1];
if (filename) {
domainText = filename;
}
} else if (document.domain && document.domain !== document.title) {
domainText = document.domain;
}

return domainText;
}

/**
* Returns the title text from an annotation and crops it to 30 chars
* if needed.
*/
function titleTextFromAnnotation(annotation: Annotation): string {
const document = documentMetadata(annotation);

let titleText = document.title;
if (titleText.length > 30) {
titleText = titleText.slice(0, 30) + '…';
}

return titleText;
}
2 changes: 2 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { documentMetadata } from './annotation-metadata';
export { processAndReplaceMentionElements } from './mentions';

export type { DocumentMetadata, DomainAndTitle } from './annotation-metadata';
export type { Group, GroupType } from './groups';
export type { Mention, MentionMode, InvalidMentionContent } from './mentions';
Loading