diff options
-rw-r--r-- | src/client/util/LinkManager.ts | 103 | ||||
-rw-r--r-- | src/client/views/DocumentButtonBar.tsx | 4 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentLinksButton.tsx | 22 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 3 | ||||
-rw-r--r-- | src/client/views/nodes/ScreenshotBox.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 10 | ||||
-rw-r--r-- | src/fields/Doc.ts | 2 | ||||
-rw-r--r-- | src/fields/util.ts | 3 |
9 files changed, 92 insertions, 59 deletions
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index bf973c3d6..62338e691 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,21 +1,21 @@ import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, Opt } from "../../fields/Doc"; -import { BoolCast, Cast, StrCast } from "../../fields/Types"; +import { Doc, DocListCast, Opt, DirectLinksSym, Field } from "../../fields/Doc"; +import { BoolCast, Cast, StrCast, PromiseValue } from "../../fields/Types"; import { LightboxView } from "../views/LightboxView"; import { DocumentViewSharedProps, ViewAdjustment } from "../views/nodes/DocumentView"; import { DocumentManager } from "./DocumentManager"; import { SharingManager } from "./SharingManager"; import { UndoManager } from "./UndoManager"; +import { observe, observable, reaction } from "mobx"; +import { listSpec } from "../../fields/Schema"; +import { List } from "../../fields/List"; +import { ProxyField } from "../../fields/Proxy"; type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; /* * link doc: - * - anchor1: doc - * - anchor1page: number - * - anchor1groups: list of group docs representing the groups anchor1 categorizes this link/anchor2 in + * - anchor1: doc * - anchor2: doc - * - anchor2page: number - * - anchor2groups: list of group docs representing the groups anchor2 categorizes this link/anchor1 in * * group doc: * - type: string representing the group type/name/category @@ -26,38 +26,80 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo */ export class LinkManager { - private static _instance: LinkManager; + @observable static _instance: LinkManager; + @observable static userDocs: Doc[] = []; public static currentLink: Opt<Doc>; - public static get Instance(): LinkManager { return this._instance || (this._instance = new this()); } + public static get Instance() { return LinkManager._instance; } + constructor() { + LinkManager._instance = this; + setTimeout(() => { + LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase as Doc)]; + const addLinkToDoc = (link: Doc): any => { + const a1 = link?.anchor1; + const a2 = link?.anchor2; + if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => addLinkToDoc(link))); + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].add(link); + Doc.GetProto(a2)[DirectLinksSym].add(link); + } + } + const remLinkFromDoc = (link: Doc): any => { + const a1 = link?.anchor1; + const a2 = link?.anchor2; + if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => remLinkFromDoc(link))); + if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { + Doc.GetProto(a1)[DirectLinksSym].delete(link); + Doc.GetProto(a2)[DirectLinksSym].delete(link); + } + } + const watchUserLinks = (userLinks: List<Doc>) => { + const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields + observe(userLinks, change => { + switch (change.type) { + case "splice": + (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link))); + (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link))); + break; + case "update": let oldValue = change.oldValue; + } + }, true); + } + observe(LinkManager.userDocs, change => { + switch (change.type) { + case "splice": (change as any).added.forEach(watchUserLinks); break; + case "update": let oldValue = change.oldValue; + } + }, true); + }); + } - public addLink(linkDoc: Doc) { return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); } + public addLink(linkDoc: Doc) { + return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); + } public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); } public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } public getAllRelatedLinks(anchor: Doc) { return this.relatedLinker(anchor); } // finds all links that contain the given anchor - public getAllDirectLinks(anchor: Doc): Doc[] { return this.directLinker(anchor); } // finds all links that contain the given anchor - public getAllLinks(): Doc[] { return this.allLinks(); } - - allLinks = computedFn(function allLinks(this: any): Doc[] { - const linkData = Doc.LinkDBDoc().data; - const lset = new Set<Doc>(DocListCast(linkData)); - SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc))); - return Array.from(lset); - }, true); + public getAllDirectLinks(anchor: Doc): Doc[] { return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); } // finds all links that contain the given anchor + public getAllLinks(): Doc[] { return []; }//this.allLinks(); } - directLinker = computedFn(function directLinker(this: any, anchor: Doc): Doc[] { - return LinkManager.Instance.allLinks().filter(link => { - const a1 = Cast(link?.anchor1, Doc, null); - const a2 = Cast(link?.anchor2, Doc, null); - return link && ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (Doc.AreProtosEqual(anchor, a1) || Doc.AreProtosEqual(anchor, a2) || Doc.AreProtosEqual(link, anchor)); - }); - }, true); + // allLinks = computedFn(function allLinks(this: any): Doc[] { + // const linkData = Doc.LinkDBDoc().data; + // const lset = new Set<Doc>(DocListCast(linkData)); + // SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc))); + // LinkManager.Instance.allLinks().filter(link => { + // const a1 = Cast(link?.anchor1, Doc, null); + // const a2 = Cast(link?.anchor2, Doc, null); + // return link && ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (Doc.AreProtosEqual(anchor, a1) || Doc.AreProtosEqual(anchor, a2) || Doc.AreProtosEqual(link, anchor)); + // }); + // return Array.from(lset); + // }, true); relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { const lfield = Doc.LayoutFieldKey(anchor); return DocListCast(anchor[lfield + "-annotations"]).concat(DocListCast(anchor[lfield + "-annotations-timeline"])).reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], - LinkManager.Instance.directLinker(anchor).slice()); + Array.from(Doc.GetProto(anchor)[DirectLinksSym]).slice());// LinkManager.Instance.directLinker(anchor).slice()); }, true); // returns map of group type to anchor's links in that group type @@ -76,13 +118,6 @@ export class LinkManager { return anchorGroups; } - // checks if a link with the given anchors exists - public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean { - return -1 !== LinkManager.Instance.allLinks().findIndex(linkDoc => - (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) || - (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1))); - } - // finds the opposite anchor of a given anchor in a link //TODO This should probably return undefined if there isn't an opposite anchor //TODO This should also await the return value of the anchor so we don't filter out promises diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index e248ef39a..a5d80cd22 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -352,10 +352,10 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const considerPush = isText && this.considerGoogleDocsPush; return <div className="documentButtonBar"> <div className="documentButtonBar-button"> - <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> + <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} /> </div> {DocumentLinksButton.StartLink || !Doc.UserDoc()["documentLinksButton-fullMenu"] ? <div className="documentButtonBar-button"> - <DocumentLinksButton links={this.view0.allLinks} View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> + <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} /> </div> : (null)} {!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button"> {this.templateButton} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 92f6ae028..60327f1bf 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -7,6 +7,7 @@ import { DocServer } from "../DocServer"; import { AssignAllExtensions } from "../../extensions/General/Extensions"; import { Networking } from "../Network"; import { CollectionView } from "./collections/CollectionView"; +import { LinkManager } from "../util/LinkManager"; AssignAllExtensions(); @@ -31,5 +32,6 @@ AssignAllExtensions(); d.setTime(d.getTime() + (100 * 24 * 60 * 60 * 1000)); const expires = "expires=" + d.toUTCString(); document.cookie = `loadtime=${loading};${expires};path=/`; + new LinkManager(); ReactDOM.render(<MainView />, document.getElementById('root')); })();
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 57d1a41b6..a6d07374a 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -31,7 +31,6 @@ interface DocumentLinksButtonProps { AlwaysOn?: boolean; InMenu?: boolean; StartLink?: boolean; - links: Doc[]; } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { @@ -225,7 +224,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } })); - @action clearLinks() { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; @@ -233,15 +231,13 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @computed get filteredLinks() { const results = [] as Doc[]; - Array.from(new Set<Doc>(this.props.links)).forEach(link => { - if (!DocUtils.FilterDocs([link], this.props.View.props.docFilters(), []).length) { - if (DocUtils.FilterDocs([link.anchor2 as Doc], this.props.View.props.docFilters(), []).length) { - results.push(link); - } - if (DocUtils.FilterDocs([link.anchor1 as Doc], this.props.View.props.docFilters(), []).length) { - results.push(link); - } - } else results.push(link); + const filters = this.props.View.props.docFilters(); + Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => { + if (DocUtils.FilterDocs([link], filters, []).length || + DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || + DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) { + results.push(link); + } }); return results; } @@ -296,12 +292,12 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? (null) : this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink) ? - <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> + <Tooltip title={<div className="dash-tooltip">{title}</div>}> {this.linkButtonInner} </Tooltip> : !DocumentLinksButton.LinkEditorDocView && !this.props.InMenu ? - <Tooltip title={<><div className="dash-tooltip">{title}</div></>}> + <Tooltip title={<div className="dash-tooltip">{title}</div>}> {this.linkButtonInner} </Tooltip> : this.linkButtonInner; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index aff0efdc7..df769a407 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -795,7 +795,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps layoutKey={this.finalLayoutKey} /> {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints} {this.hideLinkButton ? (null) : - <DocumentLinksButton View={this.props.DocumentView()} links={this.allLinks} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />} + <DocumentLinksButton View={this.props.DocumentView()} Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />} {audioView} </div>; @@ -814,7 +814,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps TraceMobx(); if (this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null); - // need to use allLinks for RTF since embedded linked text anchors are not rendered with DocumentViews. All other documents render their anchors with nested DocumentViews so we just need to render the directLinks here const filtered = DocUtils.FilterDocs(this.rootDoc.type === DocumentType.RTF ? this.allLinks : this.directLinks, this.props.docFilters(), []).filter(d => !d.hidden); return filtered.map((link, i) => diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 999ccf5f6..ec97a11e4 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -23,6 +23,7 @@ import { ViewBoxAnnotatableComponent } from "../DocComponent"; import { FieldView, FieldViewProps } from './FieldView'; import "./ScreenshotBox.scss"; import { VideoBox } from "./VideoBox"; +import { TraceMobx } from "../../../fields/util"; declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has } @@ -133,6 +134,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps, S } contentFunc = () => [this.content]; render() { + TraceMobx(); return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} > <div className="videoBox-viewer" > <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 5fd897629..9b14ca390 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,18 +1,18 @@ import React = require("react"); import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, ObservableMap } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx"; import { observer } from "mobx-react"; import * as rp from 'request-promise'; -import { Dictionary } from "typescript-collections"; -import { Doc, DocListCast, StrListCast } from "../../../fields/Doc"; +import { Doc, DocListCast } from "../../../fields/Doc"; import { documentSchema } from "../../../fields/documentSchemas"; import { InkTool } from "../../../fields/InkField"; import { makeInterface } from "../../../fields/Schema"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { VideoField, AudioField, nullAudio } from "../../../fields/URLField"; +import { AudioField, nullAudio, VideoField } from "../../../fields/URLField"; import { emptyFunction, formatTime, OmitKeys, returnOne, setupMoveUpEvents, Utils } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { Networking } from "../../Network"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SelectionManager } from "../../util/SelectionManager"; import { SnappingManager } from "../../util/SnappingManager"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; @@ -24,10 +24,8 @@ import { DocumentDecorations } from "../DocumentDecorations"; import { MarqueeAnnotator } from "../MarqueeAnnotator"; import { StyleProp } from "../StyleProvider"; import { FieldView, FieldViewProps } from './FieldView'; -import { FormattedTextBoxComment } from "./formattedText/FormattedTextBoxComment"; import { LinkDocPreview } from "./LinkDocPreview"; import "./VideoBox.scss"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; const path = require('path'); type VideoDocument = makeInterface<[typeof documentSchema]>; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 953d96ffa..d0ccce9cf 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -85,6 +85,7 @@ export const DataSym = Symbol("Data"); export const LayoutSym = Symbol("Layout"); export const FieldsSym = Symbol("Fields"); export const AclSym = Symbol("Acl"); +export const DirectLinksSym = Symbol("DirectLinks"); export const AclUnset = Symbol("AclUnset"); export const AclPrivate = Symbol("AclOwnerOnly"); export const AclReadonly = Symbol("AclReadOnly"); @@ -185,6 +186,7 @@ export class Doc extends RefField { @observable private ___fields: any = {}; @observable private ___fieldKeys: any = {}; @observable public [AclSym]: { [key: string]: symbol }; + @observable public [DirectLinksSym]: Set<Doc> = new Set(); private [UpdatingFromServer]: boolean = false; private [ForceServerWrite]: boolean = false; diff --git a/src/fields/util.ts b/src/fields/util.ts index 631cb7160..6c9c9d45c 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -19,12 +19,11 @@ function _readOnlySetter(): never { throw new Error("Documents can't be modified in read-only mode"); } -const tracing = false; +const tracing = true; export function TraceMobx() { tracing && trace(); } - export interface GetterResult { value: FieldResult; shouldReturn?: boolean; |