Browser extension that demonstrates the Web Annotation Discovery mechanism: subscribe to people’s annotation collections/‘feeds’, to see their notes on the web; and create & publish annotations yourself.

  1. import { h, Fragment, Component } from 'preact';
  2. import type { AnnotationSourceDescriptor } from '../storage/AnnotationSource';
  3. import { RpcClient } from 'webextension-rpc';
  4. import type { backgroundRpcServer } from '../background';
  5. import { MarginalAnnotations } from './MarginalAnnotations';
  6. import { ToolbarButtons } from './ToolbarButtons';
  7. import {
  8. discoverAnnotationsEmbeddedAsJSONLD,
  9. discoverAnnotationSources,
  10. } from './discovery';
  11. import { targetsUrl } from 'web-annotation-utils';
  12. import { unique } from '../util/unique';
  13. import type { IAnnotationWithSource } from '../storage/Annotation';
  14. import { AnnotationCreationHelper } from './AnnotationCreationHelper';
  15. const backgroundRpc = new RpcClient<typeof backgroundRpcServer>();
  16. const getAnnotationsForTargetUrls = backgroundRpc.func(
  17. 'getAnnotationsForTargetUrls',
  18. );
  19. const isSourceSubscribed = backgroundRpc.func('isSourceSubscribed');
  20. const updateAnnotation = backgroundRpc.func('updateAnnotation');
  21. const deleteAnnotation = backgroundRpc.func('deleteAnnotation');
  22. interface AppProps {
  23. appContainer: Node;
  24. }
  25. interface AppState {
  26. storedAnnotations: IAnnotationWithSource[];
  27. embeddedAnnotations: IAnnotationWithSource[];
  28. annotationInFocus?: IAnnotationWithSource;
  29. discoveredLinkedSources: (AnnotationSourceDescriptor & {
  30. subscribed: boolean;
  31. })[];
  32. discoveredEmbeddedSources: (AnnotationSourceDescriptor & {
  33. subscribed: boolean;
  34. })[];
  35. }
  36. export class App extends Component<AppProps, AppState> {
  37. state: AppState = {
  38. storedAnnotations: [],
  39. embeddedAnnotations: [],
  40. discoveredLinkedSources: [],
  41. discoveredEmbeddedSources: [],
  42. };
  43. async componentDidMount() {
  44. try {
  45. await Promise.all([
  46. this.loadStoredAnnotations(),
  47. this.discoverEmbeddedAnnotations(),
  48. this.discoverLinkedAnnotationSources(),
  49. ]);
  50. } catch (error) {
  51. console.log(error);
  52. }
  53. }
  54. async loadStoredAnnotations() {
  55. // Find annotations in our storage that target the current page.
  56. const urls = [document.URL];
  57. const canonicalLink = document.querySelector(
  58. 'link[rel~="canonical"]',
  59. ) as HTMLLinkElement | null;
  60. if (canonicalLink) urls.push(canonicalLink.href);
  61. const storedAnnotations = await getAnnotationsForTargetUrls(urls);
  62. console.log(
  63. `We got these annotations for <${document.URL}>:`,
  64. storedAnnotations,
  65. );
  66. this.setState({
  67. storedAnnotations,
  68. });
  69. }
  70. async discoverEmbeddedAnnotations() {
  71. // Find annotations embedded inside the page.
  72. const embeddedAnnotations = discoverAnnotationsEmbeddedAsJSONLD();
  73. const embeddedAnnotationsTargetingThisPage = embeddedAnnotations.filter(
  74. (annotation) => targetsUrl(annotation.target, document.URL),
  75. );
  76. console.log(
  77. `Found ${embeddedAnnotations.length} embedded annotations in the page, of which ${embeddedAnnotationsTargetingThisPage.length} target this page itself.`,
  78. );
  79. this.setState({
  80. embeddedAnnotations: embeddedAnnotationsTargetingThisPage.map(
  81. (annotation) => ({
  82. _id: 0,
  83. source: {
  84. _id: -1,
  85. active: false,
  86. type: 'embeddedJsonld',
  87. url: document.URL,
  88. },
  89. annotation,
  90. }),
  91. ),
  92. });
  93. // A page with embedded annotations targeting *other* pages is considered an annotation source.
  94. if (
  95. embeddedAnnotations.length > embeddedAnnotationsTargetingThisPage.length
  96. ) {
  97. const pageAsAnnotationSource: AnnotationSourceDescriptor = {
  98. title: document.title,
  99. url: document.URL.split('#')[0],
  100. type: 'embeddedJsonld',
  101. };
  102. this.setState({
  103. discoveredEmbeddedSources: await this.checkDiscoveredAnnotationSources([
  104. pageAsAnnotationSource,
  105. ]),
  106. });
  107. }
  108. }
  109. async discoverLinkedAnnotationSources() {
  110. // Find annotations sources advertised by the current page.
  111. const discoveredSources = discoverAnnotationSources();
  112. this.setState({
  113. discoveredLinkedSources: await this.checkDiscoveredAnnotationSources(
  114. discoveredSources,
  115. ),
  116. });
  117. }
  118. async checkDiscoveredAnnotationSources(
  119. discoveredSources: AnnotationSourceDescriptor[],
  120. ) {
  121. // For each discovered source, note if we already have it in our database.
  122. return await Promise.all(
  123. discoveredSources.map(async (source) => ({
  124. ...source,
  125. subscribed: await isSourceSubscribed(source),
  126. })),
  127. );
  128. }
  129. async onAnnotationCreated(annotation: IAnnotationWithSource) {
  130. await this.loadStoredAnnotations();
  131. this.setState({ annotationInFocus: annotation });
  132. }
  133. onSubscriptionChange() {
  134. this.discoverLinkedAnnotationSources();
  135. this.discoverEmbeddedAnnotations();
  136. }
  137. render(
  138. { appContainer }: AppProps,
  139. {
  140. storedAnnotations,
  141. embeddedAnnotations,
  142. annotationInFocus,
  143. discoveredLinkedSources,
  144. discoveredEmbeddedSources,
  145. }: AppState,
  146. ) {
  147. const annotationsToShow = unique(
  148. [...storedAnnotations, ...embeddedAnnotations],
  149. (obj) => obj.annotation.canonical || obj.annotation.id,
  150. );
  151. const discoveredSources = [
  152. ...discoveredLinkedSources,
  153. ...discoveredEmbeddedSources,
  154. ];
  155. const toolbarButtons =
  156. discoveredSources.length > 0 ? (
  157. <ToolbarButtons
  158. {...{
  159. onChange: () => this.onSubscriptionChange(),
  160. discoveredSources,
  161. }}
  162. />
  163. ) : undefined;
  164. return (
  165. <>
  166. <MarginalAnnotations
  167. {...{
  168. annotations: annotationsToShow,
  169. annotationInFocus,
  170. toolbarButtons,
  171. appContainer,
  172. onUpdateAnnotation: async (...args) => {
  173. await updateAnnotation(...args);
  174. // messes up text while editing.
  175. // await this.loadStoredAnnotations();
  176. },
  177. onDeleteAnnotation: async (...args) => {
  178. await deleteAnnotation(...args);
  179. await this.loadStoredAnnotations();
  180. },
  181. }}
  182. />
  183. <AnnotationCreationHelper
  184. onAnnotationCreated={(annotation) =>
  185. this.onAnnotationCreated(annotation)
  186. }
  187. />
  188. </>
  189. );
  190. }
  191. }