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.

web-annotation-discovery-we.../src/components/ AnnotationSourcesList.tsx
174 lines
5.2 KiB

  1. import { Component, h, Fragment, createRef } from 'preact';
  2. import { RpcClient } from 'webextension-rpc';
  3. import type { backgroundRpcServer } from '../background';
  4. import { AnnotationSource } from '../storage/AnnotationSource';
  5. import classes from './AnnotationSourcesList.module.scss';
  6. import cls from 'classnames';
  7. import niceTime from '../util/niceTime';
  8. import { Annotation } from '../storage/Annotation';
  9. import { AnnotationsList } from './AnnotationsList';
  10. import infoIcon from '../assets/icons/info.svg';
  11. import rssIcon from '../assets/icons/rss.svg';
  12. const backgroundRpc = new RpcClient<typeof backgroundRpcServer>();
  13. // Run refresh in the background: should continue if the page/popup is closed.
  14. const refreshAnnotationSource = backgroundRpc.func('refreshAnnotationSource');
  15. const refreshAnnotationSources = backgroundRpc.func('refreshAnnotationSources');
  16. interface AnnotationSourcesListProps {
  17. withAnnotations?: boolean;
  18. }
  19. interface AnnotationSourcesListState {
  20. sources?: AnnotationSource[];
  21. totalAnnotationCount?: number;
  22. }
  23. export class AnnotationSourcesList extends Component<
  24. AnnotationSourcesListProps,
  25. AnnotationSourcesListState
  26. > {
  27. state: AnnotationSourcesListState = {};
  28. refreshButton = createRef<HTMLButtonElement>();
  29. async componentDidMount() {
  30. await this.loadData();
  31. }
  32. async loadData() {
  33. this.setState({
  34. sources: await AnnotationSource.getAll(),
  35. // sources: await AnnotationSource.getActiveSources(),
  36. totalAnnotationCount: await Annotation.count(),
  37. });
  38. }
  39. refreshAll = async () => {
  40. const button = this.refreshButton.current!;
  41. try {
  42. button.classList.add(classes.loading);
  43. button.disabled = true;
  44. await refreshAnnotationSources({ forceAll: true });
  45. await this.loadData();
  46. } catch (error) {
  47. alert(`Refreshing failed: ${error}`);
  48. throw error;
  49. } finally {
  50. button.disabled = false;
  51. button.classList.remove(classes.loading);
  52. }
  53. };
  54. refreshSource = async (source: AnnotationSource) => {
  55. try {
  56. await refreshAnnotationSource(source.data, true);
  57. } catch (error) {
  58. alert(`Refreshing failed: ${error}`);
  59. }
  60. await this.loadData();
  61. };
  62. removeSource = async (source: AnnotationSource) => {
  63. await source.delete();
  64. await this.loadData();
  65. };
  66. render(
  67. { withAnnotations }: AnnotationSourcesListProps,
  68. { sources }: AnnotationSourcesListState,
  69. ) {
  70. const sourceList = sources?.map((source) => (
  71. <li
  72. key={source.data._id}
  73. class={cls(classes.source, { [classes.active]: source.data.active })}
  74. >
  75. <div class={classes.infoAndButtons}>
  76. <div class={classes.info}>
  77. <div class={classes.title}>
  78. {source.data.active && (
  79. <>
  80. <img src={rssIcon} />{' '}
  81. </>
  82. )}
  83. “{source.data.title}”
  84. </div>
  85. <div class={classes.url}>
  86. {source.data.active ? 'Feed: ' : 'Source: '}
  87. <a href={source.data.url} target="_blank">
  88. {source.data.url}
  89. </a>
  90. </div>{' '}
  91. <div class={classes.lastUpdate}>
  92. {source.data.active ? 'Last update: ' : 'Imported: '}
  93. {source.data.lastUpdate ? niceTime(source.data.lastUpdate) : '?'}
  94. {source.data.active || (
  95. <small> (this source is not refreshed automatically)</small>
  96. )}
  97. </div>
  98. </div>
  99. <div class={classes.buttons}>
  100. <button
  101. class={classes.button}
  102. onClick={(e) => this.removeSource(source)}
  103. >
  104. ❌ Remove
  105. </button>
  106. <button
  107. class={classes.button}
  108. onClick={(e) => this.refreshSource(source)}
  109. title={
  110. source.data.type === 'embeddedJsonld'
  111. ? `To refresh annotations embedded in a page, first open that page again.`
  112. : undefined
  113. }
  114. >
  115. 🗘 Refresh
  116. {source.data.type === 'embeddedJsonld' && (
  117. <>
  118. {' '}
  119. <img src={infoIcon} />
  120. </>
  121. )}
  122. </button>
  123. </div>
  124. </div>
  125. {withAnnotations && (
  126. <details class={classes.showAnnotations} open>
  127. <summary>Stored annotations from this source</summary>
  128. <div style="margin-left: 2em;">
  129. <AnnotationsList source={source.data} />
  130. </div>
  131. </details>
  132. )}
  133. </li>
  134. ));
  135. return (
  136. <div>
  137. {sourceList ? (
  138. <ul class={classes.sourceList}>{sourceList}</ul>
  139. ) : (
  140. <p>
  141. <i>Loading list of sources…</i>
  142. </p>
  143. )}
  144. <div style="display: flex; align-items: center;">
  145. <div style="flex-grow: 1;">
  146. Total number of annotations:{' '}
  147. {this.state.totalAnnotationCount ?? <i>'counting…'</i>}
  148. </div>
  149. <button
  150. class={classes.button}
  151. ref={this.refreshButton}
  152. onClick={this.refreshAll}
  153. >
  154. 🗘 Refresh all sources
  155. </button>
  156. </div>
  157. </div>
  158. );
  159. }
  160. }