From 565ab27220e2e626b88642929a4091d3ced893e7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 9 Oct 2020 01:14:19 -0400
Subject: fixed sharing to share with new user added during session.

---
 src/client/util/SharingManager.tsx | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index de9402ff0..1cc4c59f2 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -184,9 +184,13 @@ export class SharingManager extends React.Component<{}> {
      * @param group 
      * @param emailId 
      */
-    shareWithAddedMember = (group: Doc, emailId: string) => {
-        const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
-        if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc));
+    shareWithAddedMember = (group: Doc, emailId: string, retry: boolean = true) => {
+        const user = this.users.find(({ user: { email } }) => email === emailId)!;
+        const self = this;
+        if (group.docsShared) {
+            if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false));
+            else DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc));
+        }
     }
 
     /**
-- 
cgit v1.2.3-70-g09d2


From c8e8cbe52a97835342c3a3d04033462f4497cd72 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 9 Oct 2020 11:36:56 -0400
Subject: trying to get group updates to work automatically

---
 src/client/documents/Documents.ts   |  2 ++
 src/client/util/CurrentUserUtils.ts |  7 ++--
 src/client/util/GroupManager.tsx    | 71 ++++++++++++-------------------------
 src/client/util/GroupMemberView.tsx |  4 +--
 src/client/util/SharingManager.tsx  | 24 ++++++-------
 src/fields/util.ts                  | 39 ++++++++++++--------
 6 files changed, 67 insertions(+), 80 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7ee8267f8..7d78bd76a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -244,6 +244,7 @@ export namespace Docs {
                 view: LayoutSource,
                 dataField: string
             },
+            data?: any,
             options?: Partial<DocumentOptions>
         };
         type TemplateMap = Map<DocumentType, PrototypeTemplate>;
@@ -464,6 +465,7 @@ export namespace Docs {
             const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
             options.layout = layout.view?.LayoutString(layout.dataField);
             const doc = Doc.assign(new Doc(prototypeId, true), { system: true, layoutKey: "layout", ...options });
+            doc.data = template.data;
             doc.layout_keyValue = KeyValueBox.LayoutString("");
             return doc;
         }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d011d7b09..b4e24f70e 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -30,7 +30,7 @@ import { Scripting } from "./Scripting";
 import { SearchUtil } from "./SearchUtil";
 import { SelectionManager } from "./SelectionManager";
 import { UndoManager } from "./UndoManager";
-import { SharingPermissions } from "../../fields/util";
+import { SharingPermissions, UserGroups } from "../../fields/util";
 import { Networking } from "../Network";
 
 
@@ -988,9 +988,10 @@ export class CurrentUserUtils {
         this.setupDockedButtons(doc);  // the bottom bar of font icons
         await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
         await this.setupMenuPanel(doc, sharingDocumentId);
-        doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
-        doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
+        if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
+        if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
         if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]);
+        UserGroups.setCurrentUserGroups((doc.globalGroupDatabase as Doc).data as List<Doc>);
 
         setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered
 
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 48e3ca737..a0db46fdb 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -1,19 +1,18 @@
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
+import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import * as React from "react";
 import Select from 'react-select';
 import * as RequestPromise from "request-promise";
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
+import { Doc, DocListCast, Opt } from "../../fields/Doc";
 import { Cast, StrCast } from "../../fields/Types";
-import { setGroups } from "../../fields/util";
 import { Utils } from "../../Utils";
-import { DocServer } from "../DocServer";
 import { MainViewModal } from "../views/MainViewModal";
 import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
 import "./GroupManager.scss";
 import { GroupMemberView } from "./GroupMemberView";
 import { SharingManager, User } from "./SharingManager";
+import { listSpec } from "../../fields/Schema";
 
 /**
  * Interface for options for the react-select component
@@ -34,12 +33,8 @@ export class GroupManager extends React.Component<{}> {
     @observable private createGroupModalOpen: boolean = false;
     private inputRef: React.RefObject<HTMLInputElement> = React.createRef(); // the ref for the input box.
     private createGroupButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // the ref for the group creation button
-    private currentUserGroups: string[] = []; // the list of groups the current user is a member of
     @observable private buttonColour: "#979797" | "black" = "#979797";
     @observable private groupSort: "ascending" | "descending" | "none" = "none";
-    private populating: boolean = false;
-
-
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -51,32 +46,15 @@ export class GroupManager extends React.Component<{}> {
      */
     componentDidMount() {
         this.populateUsers();
-        this.populateGroups();
     }
 
     /**
      * Fetches the list of users stored on the database.
      */
     populateUsers = async () => {
-        if (!this.populating) {
-            const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
-            const raw = JSON.parse(userList) as User[];
-            raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email)));
-        }
-    }
-
-    /**
-     * Populates the list of groups the current user is a member of and sets this list to be used in the GetEffectiveAcl in util.ts
-     */
-    populateGroups = () => {
-        DocListCastAsync(this.GroupManagerDoc?.data).then(groups => {
-            groups?.forEach(group => {
-                const members: string[] = JSON.parse(StrCast(group.members));
-                if (members.includes(Doc.CurrentUserEmail)) this.currentUserGroups.push(StrCast(group.groupName));
-            });
-            this.currentUserGroups.push("Public");
-            setGroups(this.currentUserGroups);
-        });
+        const userList = await RequestPromise.get(Utils.prepend("/getUsers"));
+        const raw = JSON.parse(userList) as User[];
+        raw.map(action(user => !this.users.some(umail => umail === user.email) && this.users.push(user.email)));
     }
 
     /**
@@ -94,7 +72,6 @@ export class GroupManager extends React.Component<{}> {
         // SelectionManager.DeselectAll();
         this.isOpen = true;
         this.populateUsers();
-        this.populateGroups();
     }
 
     /**
@@ -113,14 +90,14 @@ export class GroupManager extends React.Component<{}> {
     /**
      * @returns the database of groups.
      */
-    get GroupManagerDoc(): Doc | undefined {
+    @computed get GroupManagerDoc(): Doc | undefined {
         return Doc.UserDoc().globalGroupDatabase as Doc;
     }
 
     /**
      * @returns a list of all group documents.
      */
-    getAllGroups(): Doc[] {
+    @computed get allGroups(): Doc[] {
         const groupDoc = this.GroupManagerDoc;
         return groupDoc ? DocListCast(groupDoc.data) : [];
     }
@@ -130,7 +107,7 @@ export class GroupManager extends React.Component<{}> {
      * @param groupName 
      */
     getGroup(groupName: string): Doc | undefined {
-        const groupDoc = this.getAllGroups().find(group => group.groupName === groupName);
+        const groupDoc = this.allGroups.find(group => group.title === groupName);
         return groupDoc;
     }
 
@@ -164,15 +141,13 @@ export class GroupManager extends React.Component<{}> {
      * @param groupName 
      * @param memberEmails 
      */
+    @action
     createGroupDoc(groupName: string, memberEmails: string[] = []) {
-        const groupDoc = new Doc;
-        groupDoc.groupName = groupName.toLowerCase() === "admin" ? "Admin" : groupName;
+        const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName;
+        const groupDoc = new Doc("GROUP:" + name);
+        groupDoc.title = name;
         groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
         groupDoc.members = JSON.stringify(memberEmails);
-        if (memberEmails.includes(Doc.CurrentUserEmail)) {
-            this.currentUserGroups.push(groupName);
-            setGroups(this.currentUserGroups);
-        }
         this.addGroup(groupDoc);
     }
 
@@ -192,19 +167,19 @@ export class GroupManager extends React.Component<{}> {
      * Deletes a group from the database of group documents and @returns whether the group was deleted or not.
      * @param group 
      */
+    @action
     deleteGroup(group: Doc): boolean {
         if (group) {
             if (this.GroupManagerDoc && this.hasEditAccess(group)) {
                 Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
                 SharingManager.Instance.removeGroup(group);
-                const members: string[] = JSON.parse(StrCast(group.members));
+                const members = JSON.parse(StrCast(group.members));
                 if (members.includes(Doc.CurrentUserEmail)) {
-                    const index = this.currentUserGroups.findIndex(groupName => groupName === group.groupName);
-                    index !== -1 && this.currentUserGroups.splice(index, 1);
-                    setGroups(this.currentUserGroups);
+                    const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group);
+                    index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1);
                 }
                 if (group === this.currentGroup) {
-                    runInAction(() => this.currentGroup = undefined);
+                    this.currentGroup = undefined;
                 }
                 return true;
             }
@@ -368,13 +343,13 @@ export class GroupManager extends React.Component<{}> {
     private get groupInterface() {
 
         const sortGroups = (d1: Doc, d2: Doc) => {
-            const g1 = StrCast(d1.groupName);
-            const g2 = StrCast(d2.groupName);
+            const g1 = StrCast(d1.title);
+            const g2 = StrCast(d2.title);
 
             return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
         };
 
-        let groups = this.getAllGroups();
+        let groups = this.allGroups;
         groups = this.groupSort === "ascending" ? groups.sort(sortGroups) : this.groupSort === "descending" ? groups.sort(sortGroups).reverse() : groups;
 
         return (
@@ -408,9 +383,9 @@ export class GroupManager extends React.Component<{}> {
                         {groups.map(group =>
                             <div
                                 className="group-row"
-                                key={StrCast(group.groupName)}
+                                key={StrCast(group.title)}
                             >
-                                <div className="group-name" >{group.groupName}</div>
+                                <div className="group-name" >{group.title}</div>
                                 <div className="group-info" onClick={action(() => this.currentGroup = group)}>
                                     <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
                                 </div>
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index 4ead01e9f..d9174561c 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -33,8 +33,8 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
                     <input
                         className="group-title"
                         style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }}
-                        value={StrCast(this.props.group.groupName)}
-                        onChange={e => this.props.group.groupName = e.currentTarget.value}
+                        value={StrCast(this.props.group.title)}
+                        onChange={e => this.props.group.title = e.currentTarget.value}
                         disabled={!hasEditAccess}
                     >
                     </input>
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 1cc4c59f2..a9abeedaf 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -152,10 +152,10 @@ export class SharingManager extends React.Component<{}> {
      * @param group 
      * @param permission 
      */
-    setInternalGroupSharing = (group: Doc | { groupName: string }, permission: string, targetDoc?: Doc) => {
+    setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
 
         const target = targetDoc || this.targetDoc!;
-        const key = normalizeEmail(StrCast(group.groupName));
+        const key = normalizeEmail(StrCast(group.title));
         const acl = `acl-${key}`;
 
         const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document);
@@ -233,7 +233,7 @@ export class SharingManager extends React.Component<{}> {
     removeGroup = (group: Doc) => {
         if (group.docsShared) {
             DocListCast(group.docsShared).forEach(doc => {
-                const acl = `acl-${StrCast(group.groupName)}`;
+                const acl = `acl-${StrCast(group.title)}`;
 
                 distributeAcls(acl, SharingPermissions.None, doc);
 
@@ -411,8 +411,8 @@ export class SharingManager extends React.Component<{}> {
      * Sorting algorithm to sort groups.
      */
     sortGroups = (group1: Doc, group2: Doc) => {
-        const g1 = StrCast(group1.groupName);
-        const g2 = StrCast(group2.groupName);
+        const g1 = StrCast(group1.title);
+        const g2 = StrCast(group2.title);
         return g1 < g2 ? -1 : g1 === g2 ? 0 : 1;
     }
 
@@ -421,7 +421,7 @@ export class SharingManager extends React.Component<{}> {
      */
     @computed get sharingInterface() {
         TraceMobx();
-        const groupList = GroupManager.Instance?.getAllGroups() || [];
+        const groupList = GroupManager.Instance?.allGroups || [];
         const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email }));
         const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) }));
 
@@ -527,19 +527,19 @@ export class SharingManager extends React.Component<{}> {
 
 
         // the list of groups shared with
-        const groupListMap: (Doc | { groupName: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(groupName))}`) : true);
-        groupListMap.unshift({ groupName: "Public" }, { groupName: "Override" });
+        const groupListMap: (Doc | { title: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(groupName))}`) : true);
+        groupListMap.unshift({ title: "Public" }, { title: "Override" });
         const groupListContents = groupListMap.map(group => {
-            const groupKey = `acl-${StrCast(group.groupName)}`;
+            const groupKey = `acl-${StrCast(group.title)}`;
             const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]);
-            const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.groupName)}`]) : "-multiple-";
+            const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : "-multiple-";
 
             return !permissions ? (null) : (
                 <div
                     key={groupKey}
                     className={"container"}
                 >
-                    <div className={"padding"}>{group.groupName}</div>
+                    <div className={"padding"}>{group.title}</div>
                     {group instanceof Doc ?
                         (<div className="group-info" onClick={action(() => GroupManager.Instance.currentGroup = group)}>
                             <FontAwesomeIcon icon={"info-circle"} color={"#e8e8e8"} size={"sm"} style={{ backgroundColor: "#1e89d7", borderRadius: "100%", border: "1px solid #1e89d7" }} />
@@ -552,7 +552,7 @@ export class SharingManager extends React.Component<{}> {
                                 value={permissions}
                                 onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}
                             >
-                                {this.sharingOptions(uniform, group.groupName === "Override")}
+                                {this.sharingOptions(uniform, group.title === "Override")}
                             </select>
                         ) : (
                                 <div className={"permissions-dropdown"}>
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 7293db0c2..1e6707a9a 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -4,7 +4,7 @@ import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField, PrefetchProxy } from "./Proxy";
 import { RefField } from "./RefField";
 import { ObjectField } from "./ObjectField";
-import { action, trace } from "mobx";
+import { action, trace, observable, reaction } from "mobx";
 import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols";
 import { DocServer } from "../client/DocServer";
 import { ComputedField } from "./ScriptField";
@@ -22,6 +22,24 @@ export function TraceMobx() {
     tracing && trace();
 }
 
+// the list of groups that the current user is a member of 
+export class UserGroups {
+    @observable static Current: string[];
+    @action static setCurrentUserGroups(groupList: List<Doc>) {
+        reaction(() => groupList,
+            groupList => {
+                UserGroups.Current = [
+                    "Public",
+                    ...(groupList as List<Doc>).
+                        filter(group => group instanceof Doc).
+                        map(group => group as Doc).
+                        filter(group => JSON.parse(StrCast(group.members))?.includes(Doc.CurrentUserEmail)).
+                        map(group => StrCast(group.title))
+                ];
+            }, { fireImmediately: true });
+    }
+}
+
 export interface GetterResult {
     value: FieldResult;
     shouldReturn?: boolean;
@@ -130,14 +148,6 @@ export function denormalizeEmail(email: string) {
 //     playgroundMode = !playgroundMode;
 // }
 
-// the list of groups that the current user is a member of 
-let currentUserGroups: string[] = [];
-
-// called from GroupManager once the groups have been fetched from the server
-export function setGroups(groups: string[]) {
-    currentUserGroups = groups;
-}
-
 /**
  * These are the various levels of access a user can have to a document.
  * 
@@ -164,6 +174,7 @@ export enum SharingPermissions {
  */
 export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol {
     if (!target) return AclPrivate;
+    if (target[Id] === "groupdbProto" || target[Id]?.startsWith("GROUP:")) return AclAdmin;
 
     // all changes received fromt the server must be processed as Admin
     if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin;
@@ -171,7 +182,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number,
     // if the current user is the author of the document / the current user is a member of the admin group
     const userChecked = user || Doc.CurrentUserEmail;
     if (userChecked === (target.__fields?.author || target.author)) return AclAdmin;
-    if (currentUserGroups.includes("Admin")) return AclAdmin;
+    if (UserGroups.Current?.includes("Admin")) return AclAdmin;
 
 
     if (target[AclSym] && Object.keys(target[AclSym]).length) {
@@ -192,7 +203,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number,
             // there are issues with storing fields with . in the name, so they are replaced with _ during creation
             // as a result we need to restore them again during this comparison.
             const entity = denormalizeEmail(key.substring(4)); // an individual or a group
-            if (currentUserGroups.includes(entity) || userChecked === entity) {
+            if (UserGroups.Current?.includes(entity) || userChecked === entity) {
                 if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
                     effectiveAcl = value as symbol;
                     if (effectiveAcl === AclAdmin) return effectiveAcl;
@@ -308,12 +319,10 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
 export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
     let prop = in_prop;
 
-    if (in_prop === "toString" || in_prop === ToString || in_prop === ToScriptString || in_prop === FieldsSym || in_prop === Id || in_prop === HandleUpdate || in_prop === CachedUpdates) return target.__fields[prop] || target[prop];
     if (in_prop === AclSym) return _overrideAcl ? undefined : target[AclSym];
+    if (in_prop === "toString" || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === "symbol")) return target.__fields[prop] || target[prop];
     if (GetEffectiveAcl(target) === AclPrivate && !_overrideAcl) return prop === HeightSym || prop === WidthSym ? returnZero : undefined;
-    if (prop === LayoutSym) {
-        return target.__LAYOUT__;
-    }
+    if (prop === LayoutSym) return target.__LAYOUT__;
     if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
         if (!prop.startsWith("_")) {
             console.log(prop + " is deprecated - switch to _" + prop);
-- 
cgit v1.2.3-70-g09d2


From ec8673566721a4625466401502c2e16878658007 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 9 Oct 2020 13:02:35 -0400
Subject: less broken, but still not working

---
 src/client/util/CurrentUserUtils.ts | 13 +++++++------
 src/client/util/GroupManager.tsx    | 12 +++++-------
 src/client/util/SharingManager.tsx  |  4 ++--
 3 files changed, 14 insertions(+), 15 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index b4e24f70e..759fd6eca 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -10,10 +10,12 @@ import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
 import { ComputedField, ScriptField } from "../../fields/ScriptField";
 import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
 import { nullAudio } from "../../fields/URLField";
+import { SharingPermissions, UserGroups } from "../../fields/util";
 import { Utils } from "../../Utils";
 import { DocServer } from "../DocServer";
 import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
 import { DocumentType } from "../documents/DocumentTypes";
+import { Networking } from "../Network";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
 import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
@@ -30,8 +32,6 @@ import { Scripting } from "./Scripting";
 import { SearchUtil } from "./SearchUtil";
 import { SelectionManager } from "./SelectionManager";
 import { UndoManager } from "./UndoManager";
-import { SharingPermissions, UserGroups } from "../../fields/util";
-import { Networking } from "../Network";
 
 
 export let resolvedPorts: { server: number, socket: number };
@@ -231,7 +231,7 @@ export class CurrentUserUtils {
         } else {
             const curButnTypes = Cast(doc["template-buttons"], Doc, null);
             DocListCastAsync(curButnTypes.data).then(async curBtns => {
-                await Promise.all(curBtns!);
+                curBtns && await Promise.all(curBtns);
                 requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype));
             });
         }
@@ -280,7 +280,7 @@ export class CurrentUserUtils {
             const curNoteTypes = Cast(doc["template-notes"], Doc, null);
             const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc];//, doc["template-note-Todo"] as any as Doc];
             DocListCastAsync(curNoteTypes.data).then(async curNotes => {
-                await Promise.all(curNotes!);
+                curNotes && await Promise.all(curNotes);
                 requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype));
             });
         }
@@ -351,7 +351,7 @@ export class CurrentUserUtils {
             const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc,
             doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc];
             DocListCastAsync(templateIconsDoc.data).then(async curIcons => {
-                await Promise.all(curIcons!);
+                curIcons && await Promise.all(curIcons);
                 requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype));
             });
         }
@@ -989,8 +989,9 @@ export class CurrentUserUtils {
         await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
         await this.setupMenuPanel(doc, sharingDocumentId);
         if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
-        if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
+        doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
         if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]);
+        const x = await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
         UserGroups.setCurrentUserGroups((doc.globalGroupDatabase as Doc).data as List<Doc>);
 
         setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index a0db46fdb..f579f3dae 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -1,11 +1,11 @@
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as React from "react";
 import Select from 'react-select';
 import * as RequestPromise from "request-promise";
-import { Doc, DocListCast, Opt } from "../../fields/Doc";
-import { Cast, StrCast } from "../../fields/Types";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
+import { StrCast, Cast } from "../../fields/Types";
 import { Utils } from "../../Utils";
 import { MainViewModal } from "../views/MainViewModal";
 import { TaskCompletionBox } from "../views/nodes/TaskCompletedBox";
@@ -98,8 +98,7 @@ export class GroupManager extends React.Component<{}> {
      * @returns a list of all group documents.
      */
     @computed get allGroups(): Doc[] {
-        const groupDoc = this.GroupManagerDoc;
-        return groupDoc ? DocListCast(groupDoc.data) : [];
+        return DocListCast(this.GroupManagerDoc?.data);
     }
 
     /**
@@ -141,10 +140,9 @@ export class GroupManager extends React.Component<{}> {
      * @param groupName 
      * @param memberEmails 
      */
-    @action
     createGroupDoc(groupName: string, memberEmails: string[] = []) {
         const name = groupName.toLowerCase() === "admin" ? "Admin" : groupName;
-        const groupDoc = new Doc("GROUP:" + name);
+        const groupDoc = new Doc("GROUP:" + name, true);
         groupDoc.title = name;
         groupDoc.owners = JSON.stringify([Doc.CurrentUserEmail]);
         groupDoc.members = JSON.stringify(memberEmails);
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index a9abeedaf..729742890 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -423,7 +423,7 @@ export class SharingManager extends React.Component<{}> {
         TraceMobx();
         const groupList = GroupManager.Instance?.allGroups || [];
         const sortedUsers = this.users.slice().sort(this.sortUsers).map(({ user: { email } }) => ({ label: email, value: indType + email }));
-        const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ groupName }) => ({ label: StrCast(groupName), value: groupType + StrCast(groupName) }));
+        const sortedGroups = groupList.slice().sort(this.sortGroups).map(({ title }) => ({ label: StrCast(title), value: groupType + StrCast(title) }));
 
         // the next block handles the users shown (individuals/groups/both)
         const options: GroupedOptions[] = [];
@@ -527,7 +527,7 @@ export class SharingManager extends React.Component<{}> {
 
 
         // the list of groups shared with
-        const groupListMap: (Doc | { title: string })[] = groups.filter(({ groupName }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(groupName))}`) : true);
+        const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true);
         groupListMap.unshift({ title: "Public" }, { title: "Override" });
         const groupListContents = groupListMap.map(group => {
             const groupKey = `acl-${StrCast(group.title)}`;
-- 
cgit v1.2.3-70-g09d2


From 2608cc4dbf852a318b9d6a453bdaf64249917cda Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 9 Oct 2020 16:54:21 -0400
Subject: fixed updating groups to take effect immediately

---
 src/client/util/CurrentUserUtils.ts |  6 +++---
 src/client/util/SharingManager.tsx  | 12 +++++++-----
 src/fields/util.ts                  | 26 +++++++++++++++++---------
 3 files changed, 27 insertions(+), 17 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 759fd6eca..580c6040e 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -956,6 +956,9 @@ export class CurrentUserUtils {
     }
 
     static async updateUserDocument(doc: Doc, sharingDocumentId: string) {
+        if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
+        await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
+        UserGroups.Current;
         doc.system = true;
         doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode;
         doc.title = Doc.CurrentUserEmail;
@@ -989,10 +992,7 @@ export class CurrentUserUtils {
         await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
         await this.setupMenuPanel(doc, sharingDocumentId);
         if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
-        doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
         if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]);
-        const x = await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
-        UserGroups.setCurrentUserGroups((doc.globalGroupDatabase as Doc).data as List<Doc>);
 
         setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered
 
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 729742890..ee397ab4f 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
 import * as React from "react";
 import Select from "react-select";
 import * as RequestPromise from "request-promise";
-import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym, AclAddonly, AclEdit, AclReadonly } from "../../fields/Doc";
+import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym, AclAddonly, AclEdit, AclReadonly, DocListCastAsync } from "../../fields/Doc";
 import { List } from "../../fields/List";
 import { Cast, StrCast } from "../../fields/Types";
 import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx, normalizeEmail } from "../../fields/util";
@@ -189,7 +189,9 @@ export class SharingManager extends React.Component<{}> {
         const self = this;
         if (group.docsShared) {
             if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false));
-            else DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc));
+            else DocListCastAsync(group.docsShared).then(dl => dl?.forEach(doc => {
+                Doc.AddDocToList(user.sharingDoc, storage, doc);
+            }));
         }
     }
 
@@ -220,9 +222,9 @@ export class SharingManager extends React.Component<{}> {
         const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
 
         if (group.docsShared) {
-            DocListCast(group.docsShared).forEach(doc => {
-                Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.sharingDoc, storage, doc); // remove the doc only if it is in the list
-            });
+            DocListCastAsync(group.docsShared).then(dl => dl?.forEach(doc => {
+                Doc.RemoveDocFromList(user.sharingDoc, storage, doc);
+            }));
         }
     }
 
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 1e6707a9a..791b98b83 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -4,7 +4,7 @@ import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField, PrefetchProxy } from "./Proxy";
 import { RefField } from "./RefField";
 import { ObjectField } from "./ObjectField";
-import { action, trace, observable, reaction } from "mobx";
+import { action, trace, observable, reaction, computed } from "mobx";
 import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols";
 import { DocServer } from "../client/DocServer";
 import { ComputedField } from "./ScriptField";
@@ -24,19 +24,28 @@ export function TraceMobx() {
 
 // the list of groups that the current user is a member of 
 export class UserGroups {
-    @observable static Current: string[];
-    @action static setCurrentUserGroups(groupList: List<Doc>) {
-        reaction(() => groupList,
-            groupList => {
-                UserGroups.Current = [
+    static computing = false;
+    static cachedGroups: string[] = [];
+    static globalGroupDoc: Doc | undefined;
+    static get Current() {
+        if (!Doc.UserDoc() || UserGroups.computing) return UserGroups.cachedGroups;
+        UserGroups.computing = true;
+        if (!UserGroups.globalGroupDoc) UserGroups.globalGroupDoc = Doc.UserDoc().globalGroupDatabase as Doc;
+        if (UserGroups.globalGroupDoc) {
+            const dbgroups = DocListCast(UserGroups.globalGroupDoc.data);
+            if (dbgroups.length !== UserGroups.cachedGroups.length - 1) {
+                UserGroups.cachedGroups = [
                     "Public",
-                    ...(groupList as List<Doc>).
+                    ...dbgroups?.
                         filter(group => group instanceof Doc).
                         map(group => group as Doc).
                         filter(group => JSON.parse(StrCast(group.members))?.includes(Doc.CurrentUserEmail)).
                         map(group => StrCast(group.title))
                 ];
-            }, { fireImmediately: true });
+            }
+        }
+        UserGroups.computing = false;
+        return UserGroups.cachedGroups;
     }
 }
 
@@ -174,7 +183,6 @@ export enum SharingPermissions {
  */
 export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol {
     if (!target) return AclPrivate;
-    if (target[Id] === "groupdbProto" || target[Id]?.startsWith("GROUP:")) return AclAdmin;
 
     // all changes received fromt the server must be processed as Admin
     if (in_prop === UpdatingFromServer || target[UpdatingFromServer]) return AclAdmin;
-- 
cgit v1.2.3-70-g09d2


From f3d04b73a53d7680092ce925fceede4f910df5bb Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 10 Oct 2020 12:10:48 -0400
Subject: fixes to issues with updating interactively whe groups change.  fixes
 for multiple rapid modifications to list field where values would be
 overwrriten when sever sendBack stale information.  fixes to list.splice()
 where deleting nothing but adding something didn't result in an addToSet.

---
 src/client/util/CurrentUserUtils.ts                | 14 +++++--
 src/client/util/GroupManager.tsx                   |  5 +++
 src/client/util/SharingManager.tsx                 | 28 ++++++++-----
 src/client/util/SnappingManager.ts                 |  8 ++++
 .../views/collections/CollectionStackingView.tsx   |  1 -
 src/fields/List.ts                                 |  5 ++-
 src/fields/util.ts                                 | 46 ++++++----------------
 src/server/websocket.ts                            |  6 ++-
 8 files changed, 61 insertions(+), 52 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 580c6040e..694982fea 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -8,9 +8,9 @@ import { RichTextField } from "../../fields/RichTextField";
 import { listSpec } from "../../fields/Schema";
 import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
 import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
+import { BoolCast, Cast, NumCast, PromiseValue, StrCast, DateCast } from "../../fields/Types";
 import { nullAudio } from "../../fields/URLField";
-import { SharingPermissions, UserGroups } from "../../fields/util";
+import { SharingPermissions } from "../../fields/util";
 import { Utils } from "../../Utils";
 import { DocServer } from "../DocServer";
 import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
@@ -32,6 +32,7 @@ import { Scripting } from "./Scripting";
 import { SearchUtil } from "./SearchUtil";
 import { SelectionManager } from "./SelectionManager";
 import { UndoManager } from "./UndoManager";
+import { SnappingManager } from "./SnappingManager";
 
 
 export let resolvedPorts: { server: number, socket: number };
@@ -957,8 +958,13 @@ export class CurrentUserUtils {
 
     static async updateUserDocument(doc: Doc, sharingDocumentId: string) {
         if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
-        await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
-        UserGroups.Current;
+        const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
+        reaction(() => DateCast((doc.globalGroupDatabase as Doc).lastModified),
+            async () => {
+                const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
+                const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
+                SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]);
+            }, { fireImmediately: true });
         doc.system = true;
         doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode;
         doc.title = Doc.CurrentUserEmail;
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 63e2a8024..cc1d45a58 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -13,6 +13,7 @@ import "./GroupManager.scss";
 import { GroupMemberView } from "./GroupMemberView";
 import { SharingManager, User } from "./SharingManager";
 import { listSpec } from "../../fields/Schema";
+import { DateField } from "../../fields/DateField";
 
 /**
  * Interface for options for the react-select component
@@ -144,6 +145,7 @@ export class GroupManager extends React.Component<{}> {
      */
     addGroup(groupDoc: Doc): boolean {
         if (this.GroupManagerDoc) {
+            this.GroupManagerDoc.lastModified = new DateField;
             Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc);
             return true;
         }
@@ -158,6 +160,7 @@ export class GroupManager extends React.Component<{}> {
     deleteGroup(group: Doc): boolean {
         if (group) {
             if (this.GroupManagerDoc && this.hasEditAccess(group)) {
+                this.GroupManagerDoc.lastModified = new DateField;
                 Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
                 SharingManager.Instance.removeGroup(group);
                 const members = JSON.parse(StrCast(group.members));
@@ -184,6 +187,7 @@ export class GroupManager extends React.Component<{}> {
             const memberList = JSON.parse(StrCast(groupDoc.members));
             !memberList.includes(email) && memberList.push(email);
             groupDoc.members = JSON.stringify(memberList);
+            this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
             SharingManager.Instance.shareWithAddedMember(groupDoc, email);
         }
     }
@@ -200,6 +204,7 @@ export class GroupManager extends React.Component<{}> {
             if (index !== -1) {
                 const user = memberList.splice(index, 1)[0];
                 groupDoc.members = JSON.stringify(memberList);
+                this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
                 SharingManager.Instance.removeMember(groupDoc, email);
             }
         }
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index ee397ab4f..16bcd46c8 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -22,6 +22,7 @@ import "./SharingManager.scss";
 import { SelectionManager } from "./SelectionManager";
 import { intersection } from "lodash";
 import { SearchBox } from "../views/search/SearchBox";
+import { listSpec } from "../../fields/Schema";
 
 export interface User {
     email: string;
@@ -172,8 +173,8 @@ export class SharingManager extends React.Component<{}> {
                 group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]);
 
                 users.forEach(({ user, sharingDoc }) => {
-                    if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(sharingDoc[storage])) === -1 && Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
-                    else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
+                    if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
+                    else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
                 });
             }
         });
@@ -189,9 +190,13 @@ export class SharingManager extends React.Component<{}> {
         const self = this;
         if (group.docsShared) {
             if (!user) retry && this.populateUsers().then(() => self.shareWithAddedMember(group, emailId, false));
-            else DocListCastAsync(group.docsShared).then(dl => dl?.forEach(doc => {
-                Doc.AddDocToList(user.sharingDoc, storage, doc);
-            }));
+            else {
+                DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
+                    DocListCastAsync(group.docsShared).then(dl => {
+                        const filtered = dl?.filter(doc => !userdocs?.includes(doc));
+                        filtered && userdocs?.push(...filtered);
+                    }));
+            }
         }
     }
 
@@ -222,9 +227,12 @@ export class SharingManager extends React.Component<{}> {
         const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
 
         if (group.docsShared) {
-            DocListCastAsync(group.docsShared).then(dl => dl?.forEach(doc => {
-                Doc.RemoveDocFromList(user.sharingDoc, storage, doc);
-            }));
+            DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
+                DocListCastAsync(group.docsShared).then(dl => {
+                    const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || [];
+                    userdocs?.splice(0, userdocs.length, ...remaining);
+                })
+            );
         }
     }
 
@@ -261,8 +269,8 @@ export class SharingManager extends React.Component<{}> {
             doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc);
             distributeAcls(acl, permission as SharingPermissions, doc);
 
-            if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(sharingDoc[storage])) === -1 && Doc.AddDocToList(sharingDoc, storage, doc);
-            else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
+            if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc);
+            else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
         });
     }
 
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index fc07e8ab4..d067dff6c 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -14,6 +14,9 @@ export namespace SnappingManager {
             this.horizSnapLines = horizLines;
             this.vertSnapLines = vertLines;
         }
+
+        @observable cachedGroups: string[] = [];
+        @action setCachedGroups(groups: string[]) { this.cachedGroups = groups; }
     }
 
     const manager = new Manager();
@@ -25,5 +28,10 @@ export namespace SnappingManager {
 
     export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
     export function GetIsDragging() { return manager.IsDragging; }
+
+    /// bcz; argh!! TODO;   These do not belong here, but there were include order problems with leaving them in util.ts
+    // need to investigate further what caused the mobx update problems and move to a better location.
+    export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); }
+    export function GetCachedGroups() { return manager.cachedGroups; }
 }
 
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index daaee67dc..72ce041e1 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -294,7 +294,6 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
                 const docs = this.childDocList;
                 if (docs) {
                     newDocs.map((doc, i) => {
-                        console.log(doc.title);
                         if (i === 0) {
                             if (targInd === -1) targInd = docs.length;
                             else targInd = docs.indexOf(this.filteredChildren[targInd]);
diff --git a/src/fields/List.ts b/src/fields/List.ts
index ceb538b2d..a0cbebaf5 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -43,7 +43,7 @@ const listHandlers: any = {
             }
         }
         const res = list.__fields.push(...items);
-        this[Update]({ op: "$addToSet", items });
+        this[Update]({ op: "$addToSet", items, length: length + items.length });
         return res;
     }),
     reverse() {
@@ -77,7 +77,8 @@ const listHandlers: any = {
             }
         }
         const res = list.__fields.splice(start, deleteCount, ...items);
-        this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed } : undefined);
+        this[Update](items.length === 0 && deleteCount ? { op: "$remFromSet", items: removed, length: list.__fields.length } :
+            items.length && !deleteCount ? { op: "$addToSet", items, length: list.__fields.length } : undefined);
         return res.map(toRealField);
     }),
     unshift(...items: any[]) {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 791b98b83..fd409a54e 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
 import { UndoManager } from "../client/util/UndoManager";
-import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto, AclUnset } from "./Doc";
+import { Doc, FieldResult, UpdatingFromServer, LayoutSym, AclPrivate, AclEdit, AclReadonly, AclAddonly, AclSym, CachedUpdates, DataSym, DocListCast, AclAdmin, FieldsSym, HeightSym, WidthSym, fetchProto, AclUnset, DocListCastAsync } from "./Doc";
 import { SerializationHelper } from "../client/util/SerializationHelper";
 import { ProxyField, PrefetchProxy } from "./Proxy";
 import { RefField } from "./RefField";
@@ -8,10 +8,13 @@ import { action, trace, observable, reaction, computed } from "mobx";
 import { Parent, OnUpdate, Update, Id, SelfProxy, Self, HandleUpdate, ToString, ToScriptString } from "./FieldSymbols";
 import { DocServer } from "../client/DocServer";
 import { ComputedField } from "./ScriptField";
-import { ScriptCast, StrCast } from "./Types";
+import { ScriptCast, StrCast, DateCast, Cast, NumCast } from "./Types";
 import { returnZero } from "../Utils";
 import CursorField from "./CursorField";
 import { List } from "./List";
+import { listSpec } from "./Schema";
+import { DateField } from "./DateField";
+import { SnappingManager } from "../client/util/SnappingManager";
 
 function _readOnlySetter(): never {
     throw new Error("Documents can't be modified in read-only mode");
@@ -22,32 +25,6 @@ export function TraceMobx() {
     tracing && trace();
 }
 
-// the list of groups that the current user is a member of 
-export class UserGroups {
-    static computing = false;
-    static cachedGroups: string[] = [];
-    static globalGroupDoc: Doc | undefined;
-    static get Current() {
-        if (!Doc.UserDoc() || UserGroups.computing) return UserGroups.cachedGroups;
-        UserGroups.computing = true;
-        if (!UserGroups.globalGroupDoc) UserGroups.globalGroupDoc = Doc.UserDoc().globalGroupDatabase as Doc;
-        if (UserGroups.globalGroupDoc) {
-            const dbgroups = DocListCast(UserGroups.globalGroupDoc.data);
-            if (dbgroups.length !== UserGroups.cachedGroups.length - 1) {
-                UserGroups.cachedGroups = [
-                    "Public",
-                    ...dbgroups?.
-                        filter(group => group instanceof Doc).
-                        map(group => group as Doc).
-                        filter(group => JSON.parse(StrCast(group.members))?.includes(Doc.CurrentUserEmail)).
-                        map(group => StrCast(group.title))
-                ];
-            }
-        }
-        UserGroups.computing = false;
-        return UserGroups.cachedGroups;
-    }
-}
 
 export interface GetterResult {
     value: FieldResult;
@@ -190,7 +167,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number,
     // if the current user is the author of the document / the current user is a member of the admin group
     const userChecked = user || Doc.CurrentUserEmail;
     if (userChecked === (target.__fields?.author || target.author)) return AclAdmin;
-    if (UserGroups.Current?.includes("Admin")) return AclAdmin;
+    if (SnappingManager.GetCachedGroups().includes("Admin")) return AclAdmin;
 
 
     if (target[AclSym] && Object.keys(target[AclSym]).length) {
@@ -211,7 +188,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number,
             // there are issues with storing fields with . in the name, so they are replaced with _ during creation
             // as a result we need to restore them again during this comparison.
             const entity = denormalizeEmail(key.substring(4)); // an individual or a group
-            if (UserGroups.Current?.includes(entity) || userChecked === entity) {
+            if (SnappingManager.GetCachedGroups().includes(entity) || userChecked === entity) {
                 if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
                     effectiveAcl = value as symbol;
                     if (effectiveAcl === AclAdmin) return effectiveAcl;
@@ -388,9 +365,12 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
 export function updateFunction(target: any, prop: any, value: any, receiver: any) {
     let current = ObjectField.MakeCopy(value);
     return (diff?: any) => {
-        const op = diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } :
-            diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
-                : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+        const op =
+            diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } :
+                diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
+                    : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
+        !op['$set'] && ((op as any).length = diff.length);
+
         const oldValue = current;
         const newValue = ObjectField.MakeCopy(value);
         current = newValue;
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index c9b5d1cbf..03795d254 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -277,7 +277,8 @@ export namespace WebSocket {
         const newListItems = diff.diff.$set[updatefield].fields;
         const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || [];
         diff.diff.$set[updatefield].fields = [...curList, ...newListItems.filter((newItem: any) => !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : false))];
-        const sendBack = true;//curList.length !== prelen;
+        const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length;
+        delete diff.diff.length;
         Database.Instance.update(diff.id, diff.diff,
             () => {
                 if (sendBack) {
@@ -295,7 +296,8 @@ export namespace WebSocket {
         const remListItems = diff.diff.$set[updatefield].fields;
         const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || [];
         diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : false));
-        const sendBack = true;//curList.length + remListItems.length !== prelen;
+        const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length;
+        delete diff.diff.length;
         Database.Instance.update(diff.id, diff.diff,
             () => {
                 if (sendBack) {
-- 
cgit v1.2.3-70-g09d2


From d123cac247bcc37aa00fe44a4933df86b71e93af Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 10 Oct 2020 17:49:41 -0400
Subject: split LinkDatbase into its own field in the user's registry.  fixed
 sharing to set lastModified after modifying groups so that remote
 participants see the right data.  fixed text boxes to show blue icon when
 there's an annotaitn entry

---
 src/client/util/CurrentUserUtils.ts                | 26 ++++++++++++++--------
 src/client/util/GroupManager.tsx                   |  8 +++----
 src/client/util/LinkManager.ts                     | 12 ++++++----
 src/client/util/SharingManager.tsx                 |  9 +++++---
 .../views/nodes/formattedText/FormattedTextBox.tsx | 12 +++++-----
 src/fields/Doc.ts                                  |  1 +
 src/server/ApiManagers/UserManager.ts              | 12 +++++++---
 src/server/GarbageCollector.ts                     |  2 +-
 src/server/authentication/AuthenticationManager.ts |  1 +
 src/server/authentication/DashUserModel.ts         |  4 +++-
 10 files changed, 57 insertions(+), 30 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 694982fea..7535d7c24 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,6 +1,6 @@
 import { computed, observable, reaction } from "mobx";
 import * as rp from 'request-promise';
-import { DataSym, Doc, DocListCast, DocListCastAsync } from "../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, AclReadonly } from "../../fields/Doc";
 import { Id } from "../../fields/FieldSymbols";
 import { List } from "../../fields/List";
 import { PrefetchProxy } from "../../fields/Proxy";
@@ -541,9 +541,9 @@ export class CurrentUserUtils {
             })) as any as Doc;
         }
     }
-    static async setupMenuPanel(doc: Doc, sharingDocumentId: string) {
+    static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
         if (doc.menuStack === undefined) {
-            await this.setupSharingSidebar(doc, sharingDocumentId);  // sets up the right sidebar collection for mobile upload documents and sharing
+            await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId);  // sets up the right sidebar collection for mobile upload documents and sharing
             const menuBtns = (await CurrentUserUtils.menuBtnDescriptions(doc)).map(({ title, target, icon, click, watchedDocuments }) =>
                 Docs.Create.FontIconDocument({
                     icon,
@@ -876,7 +876,16 @@ export class CurrentUserUtils {
     }
 
     // Sharing sidebar is where shared documents are contained
-    static async setupSharingSidebar(doc: Doc, sharingDocumentId: string) {
+    static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
+        if (doc.myLinkDatabase === undefined) {
+            let linkDocs = await DocServer.GetRefField(linkDatabaseId);
+            if (!linkDocs) {
+                linkDocs = new Doc(linkDatabaseId, true);
+                (linkDocs as Doc).data = new List<Doc>([]);
+                (linkDocs as Doc)["acl-Public"] = SharingPermissions.Add;
+            }
+            doc.myLinkDatabase = new PrefetchProxy(linkDocs);
+        }
         if (doc.mySharedDocs === undefined) {
             let sharedDocs = await DocServer.GetRefField(sharingDocumentId + "outer");
             if (!sharedDocs) {
@@ -956,7 +965,7 @@ export class CurrentUserUtils {
         return doc.clickFuncs as Doc;
     }
 
-    static async updateUserDocument(doc: Doc, sharingDocumentId: string) {
+    static async updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
         if (!doc.globalGroupDatabase) doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument();
         const groups = await DocListCastAsync((doc.globalGroupDatabase as Doc).data);
         reaction(() => DateCast((doc.globalGroupDatabase as Doc).lastModified),
@@ -996,9 +1005,8 @@ export class CurrentUserUtils {
         this.setupOverlays(doc);  // documents in overlay layer
         this.setupDockedButtons(doc);  // the bottom bar of font icons
         await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
-        await this.setupMenuPanel(doc, sharingDocumentId);
+        await this.setupMenuPanel(doc, sharingDocumentId, linkDatabaseId);
         if (!doc.globalScriptDatabase) doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument();
-        if (!doc.myLinkDatabase) doc.myLinkDatabase = new List([]);
 
         setTimeout(() => this.setupDefaultPresentation(doc), 0); // presentation that's initially triggered
 
@@ -1036,10 +1044,10 @@ export class CurrentUserUtils {
     public static async loadUserDocument(id: string) {
         this.curr_id = id;
         await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => {
-            const { userDocumentId, sharingDocumentId } = JSON.parse(ids);
+            const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids);
             if (userDocumentId !== "guest") {
                 return DocServer.GetRefField(userDocumentId).then(async field =>
-                    this.updateUserDocument(Doc.SetUserDoc(field instanceof Doc ? field : new Doc(userDocumentId, true)), sharingDocumentId));
+                    this.updateUserDocument(Doc.SetUserDoc(field instanceof Doc ? field : new Doc(userDocumentId, true)), sharingDocumentId, linkDatabaseId));
             } else {
                 throw new Error("There should be a user id! Why does Dash think there isn't one?");
             }
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index cc1d45a58..6458de0ed 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -145,8 +145,8 @@ export class GroupManager extends React.Component<{}> {
      */
     addGroup(groupDoc: Doc): boolean {
         if (this.GroupManagerDoc) {
-            this.GroupManagerDoc.lastModified = new DateField;
             Doc.AddDocToList(this.GroupManagerDoc, "data", groupDoc);
+            this.GroupManagerDoc.lastModified = new DateField;
             return true;
         }
         return false;
@@ -160,7 +160,6 @@ export class GroupManager extends React.Component<{}> {
     deleteGroup(group: Doc): boolean {
         if (group) {
             if (this.GroupManagerDoc && this.hasEditAccess(group)) {
-                this.GroupManagerDoc.lastModified = new DateField;
                 Doc.RemoveDocFromList(this.GroupManagerDoc, "data", group);
                 SharingManager.Instance.removeGroup(group);
                 const members = JSON.parse(StrCast(group.members));
@@ -168,6 +167,7 @@ export class GroupManager extends React.Component<{}> {
                     const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group);
                     index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1);
                 }
+                this.GroupManagerDoc.lastModified = new DateField;
                 if (group === this.currentGroup) {
                     this.currentGroup = undefined;
                 }
@@ -187,8 +187,8 @@ export class GroupManager extends React.Component<{}> {
             const memberList = JSON.parse(StrCast(groupDoc.members));
             !memberList.includes(email) && memberList.push(email);
             groupDoc.members = JSON.stringify(memberList);
-            this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
             SharingManager.Instance.shareWithAddedMember(groupDoc, email);
+            this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
         }
     }
 
@@ -204,8 +204,8 @@ export class GroupManager extends React.Component<{}> {
             if (index !== -1) {
                 const user = memberList.splice(index, 1)[0];
                 groupDoc.members = JSON.stringify(memberList);
-                this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
                 SharingManager.Instance.removeMember(groupDoc, email);
+                this.GroupManagerDoc && (this.GroupManagerDoc.lastModified = new DateField);
             }
         }
     }
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 56b6cb8a9..0456b4029 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -36,17 +36,21 @@ export class LinkManager {
 
 
     public getAllLinks(): Doc[] {
-        const lset = new Set<Doc>(DocListCast(Doc.UserDoc().myLinkDatabase));
-        SharingManager.Instance.users.forEach(user => DocListCast(user.sharingDoc.myLinkDatabase).map(lset.add));
+        const lset = new Set<Doc>(DocListCast(Doc.LinkDBDoc().data));
+        SharingManager.Instance.users.forEach(user => {
+            DocListCast((user.linkDatabase as Doc)?.data).map(doc => {
+                lset.add(doc);
+            });
+        });
         return Array.from(lset);
     }
 
     public addLink(linkDoc: Doc): boolean {
-        return Doc.AddDocToList(Doc.UserDoc(), "myLinkDatabase", linkDoc);
+        return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc);
     }
 
     public deleteLink(linkDoc: Doc): boolean {
-        return Doc.RemoveDocFromList(Doc.UserDoc(), "myLinkDatabase", linkDoc);
+        return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc);
     }
 
     // finds all links that contain the given anchor
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 16bcd46c8..271face98 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -22,11 +22,11 @@ import "./SharingManager.scss";
 import { SelectionManager } from "./SelectionManager";
 import { intersection } from "lodash";
 import { SearchBox } from "../views/search/SearchBox";
-import { listSpec } from "../../fields/Schema";
 
 export interface User {
     email: string;
     sharingDocumentId: string;
+    linkDatabaseId: string;
 }
 
 /**
@@ -53,6 +53,7 @@ const storage = "data";
 interface ValidatedUser {
     user: User;         // database minimal info to identify / communicate with a user (email, sharing doc id)
     sharingDoc: Doc;    // document to share/message another user
+    linkDatabase: Doc;
     userColor: string;  // stored on the sharinDoc, extracted for convenience?
 }
 
@@ -130,8 +131,10 @@ export class SharingManager extends React.Component<{}> {
                 const isCandidate = user.email !== Doc.CurrentUserEmail;
                 if (isCandidate) {
                     const sharingDoc = await DocServer.GetRefField(user.sharingDocumentId);
-                    if (sharingDoc instanceof Doc) {
-                        sharingDocs.push({ user, sharingDoc, userColor: StrCast(sharingDoc.color) });
+                    const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId);
+                    if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
+                        await DocListCastAsync(linkDatabase.data);
+                        sharingDocs.push({ user, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.color) });
                     }
                 }
             });
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 598657a58..99a009d13 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -289,10 +289,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
                     }
                 }
             } else {
-
-                const json = JSON.parse(Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!);
-                json.selection = state.toJSON().selection;
-                this._editorView.updateState(EditorState.fromJSON(this.config, json));
+                const jsonstring = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Data!;
+                if (jsonstring) {
+                    const json = JSON.parse(jsonstring);
+                    json.selection = state.toJSON().selection;
+                    this._editorView.updateState(EditorState.fromJSON(this.config, json));
+                }
             }
         }
     }
@@ -1554,7 +1556,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
     }
 
     @computed get sidebarHandle() {
-        const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.title).length;
+        const annotated = DocListCast(this.dataDoc[this.annotationKey]).filter(d => d?.author).length;
         return !this.props.isSelected() && !(annotated && !this.sidebarWidth()) ? (null) :
             <div className="formattedTextBox-sidebar-handle"
                 style={{ left: `max(0px, calc(100% - ${this.sidebarWidthPercent} ${this.sidebarWidth() ? "- 5px" : "- 10px"}))`, background: annotated ? "lightBlue" : undefined }}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index d85f0785e..0e0a49876 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -885,6 +885,7 @@ export namespace Doc {
     export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
     export function UserDoc(): Doc { return manager._user_doc; }
     export function SharingDoc(): Doc { return Cast(Doc.UserDoc().mySharedDocs, Doc, null); }
+    export function LinkDBDoc(): Doc { return Cast(Doc.UserDoc().myLinkDatabase, Doc, null); }
 
     export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; }
     export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; }
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index e5c0f3827..f36506b14 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -19,9 +19,9 @@ export default class UserManager extends ApiManager {
             method: Method.GET,
             subscription: "/getUsers",
             secureHandler: async ({ res }) => {
-                const cursor = await Database.Instance.query({}, { email: 1, sharingDocumentId: 1 }, "users");
+                const cursor = await Database.Instance.query({}, { email: 1, linkDatabaseId: 1, sharingDocumentId: 1 }, "users");
                 const results = await cursor.toArray();
-                res.send(results.map(user => ({ email: user.email, sharingDocumentId: user.sharingDocumentId })));
+                res.send(results.map(user => ({ email: user.email, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId })));
             }
         });
 
@@ -47,7 +47,7 @@ export default class UserManager extends ApiManager {
         register({
             method: Method.GET,
             subscription: "/getUserDocumentIds",
-            secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, sharingDocumentId: user.sharingDocumentId })
+            secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId })
         });
 
         register({
@@ -56,6 +56,12 @@ export default class UserManager extends ApiManager {
             secureHandler: ({ res, user }) => res.send(user.sharingDocumentId)
         });
 
+        register({
+            method: Method.GET,
+            subscription: "/getLinkDatabaseId",
+            secureHandler: ({ res, user }) => res.send(user.linkDatabaseId)
+        });
+
         register({
             method: Method.GET,
             subscription: "/getCurrentUser",
diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts
index 6bd0e5163..7c441e3c0 100644
--- a/src/server/GarbageCollector.ts
+++ b/src/server/GarbageCollector.ts
@@ -65,7 +65,7 @@ async function GarbageCollect(full: boolean = true) {
     // await new Promise(res => setTimeout(res, 3000));
     const cursor = await Database.Instance.query({}, { userDocumentId: 1 }, 'users');
     const users = await cursor.toArray();
-    const ids: string[] = [...users.map(user => user.userDocumentId), ...users.map(user => user.sharingDocumentId)]];
+    const ids: string[] = [...users.map(user => user.userDocumentId), ...users.map(user => user.sharingDocumentId), ...users.map(user => user.linkDatabaseId)];
     const visited = new Set<string>();
     const files: { [name: string]: string[] } = {};
 
diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts
index 84abd41a2..9eb4a328f 100644
--- a/src/server/authentication/AuthenticationManager.ts
+++ b/src/server/authentication/AuthenticationManager.ts
@@ -49,6 +49,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
         password,
         userDocumentId: Utils.GenerateGuid(),
         sharingDocumentId: Utils.GenerateGuid(),
+        linkDatabaseId: Utils.GenerateGuid(),
         cacheDocumentIds: ""
     } as Partial<DashUserModel>;
 
diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts
index 4f2856a78..bee28b96d 100644
--- a/src/server/authentication/DashUserModel.ts
+++ b/src/server/authentication/DashUserModel.ts
@@ -11,6 +11,7 @@ export type DashUserModel = mongoose.Document & {
 
     userDocumentId: string;
     sharingDocumentId: string;
+    linkDatabaseId: string;
     cacheDocumentIds: string;
 
     profile: {
@@ -39,7 +40,8 @@ const userSchema = new mongoose.Schema({
 
     userDocumentId: String,    // id that identifies a document which hosts all of a user's account data
     sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users
-    cacheDocumentIds: String,
+    linkDatabaseId: String,
+    cacheDocumentIds: String, // set of document ids to retreive on startup
 
     facebook: String,
     twitter: String,
-- 
cgit v1.2.3-70-g09d2


From c63aeeb0010a79f0b19d8719f97d98b7a83baf3b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 10 Oct 2020 20:24:41 -0400
Subject: cached groups user is in for efficiency which avoids th whole screen
 redrawing when a group is added.

---
 src/client/util/LinkManager.ts     | 4 +---
 src/client/util/SharingManager.tsx | 4 ++--
 src/client/util/SnappingManager.ts | 5 ++++-
 src/fields/util.ts                 | 8 +++-----
 4 files changed, 10 insertions(+), 11 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 0456b4029..e74ce87f8 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,6 +1,4 @@
 import { Doc, DocListCast, Opt } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
 import { Cast, StrCast } from "../../fields/Types";
 import { SharingManager } from "./SharingManager";
 import { computedFn } from "mobx-utils";
@@ -71,7 +69,7 @@ export class LinkManager {
             related.push(...LinkManager.Instance.getAllRelatedLinks(anno));
         });
         return related;
-    }.bind(this));
+    }.bind(this), true);
 
     // finds all links that contain the given anchor
     public getAllRelatedLinks(anchor: Doc): Doc[] {
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 271face98..0c8f19eae 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -473,7 +473,7 @@ export class SharingManager extends React.Component<{}> {
         const commonKeys = intersection(...docs.map(doc => this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym])));
 
         // the list of users shared with
-        const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, sharingDoc, userColor }) => {
+        const userListContents: (JSX.Element | null)[] = users.filter(({ user }) => docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email).map(({ user, linkDatabase, sharingDoc, userColor }) => {
             const userKey = `acl-${normalizeEmail(user.email)}`;
             const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]);
             const permissions = uniform ? StrCast(targetDoc?.[userKey]) : "-multiple-";
@@ -489,7 +489,7 @@ export class SharingManager extends React.Component<{}> {
                             <select
                                 className={"permissions-dropdown"}
                                 value={permissions}
-                                onChange={e => this.setInternalSharing({ user, sharingDoc: sharingDoc, userColor }, e.currentTarget.value)}
+                                onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}
                             >
                                 {this.sharingOptions(uniform)}
                             </select>
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index d067dff6c..a615f0247 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,4 +1,5 @@
 import { observable, action, runInAction } from "mobx";
+import { computedFn } from "mobx-utils";
 
 export namespace SnappingManager {
 
@@ -32,6 +33,8 @@ export namespace SnappingManager {
     /// bcz; argh!! TODO;   These do not belong here, but there were include order problems with leaving them in util.ts
     // need to investigate further what caused the mobx update problems and move to a better location.
     export function SetCachedGroups(groups: string[]) { manager.setCachedGroups(groups); }
-    export function GetCachedGroups() { return manager.cachedGroups; }
+    export function GetCachedGroupByName(name: string) {
+        return computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true)(name);
+    }
 }
 
diff --git a/src/fields/util.ts b/src/fields/util.ts
index fd409a54e..881f301f3 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -12,8 +12,6 @@ import { ScriptCast, StrCast, DateCast, Cast, NumCast } from "./Types";
 import { returnZero } from "../Utils";
 import CursorField from "./CursorField";
 import { List } from "./List";
-import { listSpec } from "./Schema";
-import { DateField } from "./DateField";
 import { SnappingManager } from "../client/util/SnappingManager";
 
 function _readOnlySetter(): never {
@@ -167,7 +165,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number,
     // if the current user is the author of the document / the current user is a member of the admin group
     const userChecked = user || Doc.CurrentUserEmail;
     if (userChecked === (target.__fields?.author || target.author)) return AclAdmin;
-    if (SnappingManager.GetCachedGroups().includes("Admin")) return AclAdmin;
+    if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin;
 
 
     if (target[AclSym] && Object.keys(target[AclSym]).length) {
@@ -188,7 +186,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number,
             // there are issues with storing fields with . in the name, so they are replaced with _ during creation
             // as a result we need to restore them again during this comparison.
             const entity = denormalizeEmail(key.substring(4)); // an individual or a group
-            if (SnappingManager.GetCachedGroups().includes(entity) || userChecked === entity) {
+            if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) {
                 if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
                     effectiveAcl = value as symbol;
                     if (effectiveAcl === AclAdmin) return effectiveAcl;
@@ -369,7 +367,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
             diff?.op === "$addToSet" ? { '$addToSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } } :
                 diff?.op === "$remFromSet" ? { '$remFromSet': { ["fields." + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
                     : { '$set': { ["fields." + prop]: SerializationHelper.Serialize(value) } };
-        !op['$set'] && ((op as any).length = diff.length);
+        !op.$set && ((op as any).length = diff.length);
 
         const oldValue = current;
         const newValue = ObjectField.MakeCopy(value);
-- 
cgit v1.2.3-70-g09d2


From 04bbff759710d58fa97ce2f0d685ec59b6beb60e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 12 Oct 2020 12:53:40 -0400
Subject: split getEffectiveAcl code for determining if a field can be modified
 out into getPropAcl.

---
 src/client/util/SharingManager.tsx              |  4 +--
 src/client/views/DocComponent.tsx               |  2 +-
 src/client/views/collections/CollectionView.tsx |  2 +-
 src/fields/util.ts                              | 44 ++++++++++++++-----------
 4 files changed, 28 insertions(+), 24 deletions(-)

(limited to 'src/client/util/SharingManager.tsx')

diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 0c8f19eae..914253e3c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -177,7 +177,7 @@ export class SharingManager extends React.Component<{}> {
 
                 users.forEach(({ user, sharingDoc }) => {
                     if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
-                    else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
+                    else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists
                 });
             }
         });
@@ -273,7 +273,7 @@ export class SharingManager extends React.Component<{}> {
             distributeAcls(acl, permission as SharingPermissions, doc);
 
             if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, storage, doc);
-            else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
+            else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc || doc));
         });
     }
 
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 98e888538..a55f4adaf 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -164,7 +164,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
                     return false;
                 }
                 else {
-                    if (this.props.Document[AclSym]) {
+                    if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
                         added.forEach(d => {
                             for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
                                 if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index cfd24545b..a27fa5a66 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -140,7 +140,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
                 return false;
             }
             else {
-                if (this.props.Document[AclSym]) {
+                if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym])) {
                     added.forEach(d => {
                         for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
                             if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 56736028a..dd0444d61 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -155,32 +155,35 @@ export enum SharingPermissions {
 }
 
 // return acl from cache or cache the acl and return. bcz: Argh!  NOT WORKING ... nothing gets invalidated properly....
-const getEffectiveAclCache = computedFn(function (target: any, playgroundProp: boolean, user?: string) { return getEffectiveAcl(target, playgroundProp, user); }, true);
+const getEffectiveAclCache = computedFn(function (target: any, user?: string) { return getEffectiveAcl(target, user); }, true);
 
 /**
  * Calculates the effective access right to a document for the current user.
  */
-export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, user?: string): symbol {
-    if (!target) return AclPrivate;
-    if (in_prop === UpdatingFromServer) return AclAdmin;  // requesting the UpdatingFromServer prop must always go through to keep the local DB consistent
-    const playgroundProp = in_prop && DocServer.PlaygroundFields?.includes(in_prop.toString()) ? true : false;
-    return getEffectiveAcl(target, playgroundProp, user);
+export function GetEffectiveAcl(target: any, user?: string): symbol {
+    return target ? getEffectiveAcl(target, user) : AclPrivate;
 }
 
-function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): symbol {
+function getPropAcl(target: any, prop: string | symbol | number) {
+    if (prop === UpdatingFromServer) return AclAdmin;  // requesting the UpdatingFromServer prop must always go through to keep the local DB consistent
+    if (prop && DocServer.PlaygroundFields?.includes(prop.toString())) return AclEdit; // playground props are always editable
+    return GetEffectiveAcl(target);
+}
+
+let HierarchyMapping: Map<symbol, number> | undefined;
+
+function getEffectiveAcl(target: any, user?: string): symbol {
     if (target[UpdatingFromServer]) return AclAdmin; // all changes received from the server must be processed as Admin
     // if the current user is the author of the document / the current user is a member of the admin group
     const userChecked = user || Doc.CurrentUserEmail;
-    if (userChecked === (target.__fields?.author || target.author)) return AclAdmin;
+    if (userChecked === (target.__fields?.author || target.author)) return AclAdmin; // target may be a Doc of Proxy, so check __fields.author and .author
     if (SnappingManager.GetCachedGroupByName("Admin")) return AclAdmin;
+    const targetAcls = target[AclSym];
 
-    if (target[AclSym] && Object.keys(target[AclSym]).length) {
+    if (targetAcls && Object.keys(targetAcls).length) {
+        if (_overrideAcl) return AclEdit;
 
-        // if the acl is being overriden or the property being modified is one of the playground fields (which can be freely modified)
-        if (_overrideAcl || playgroundProp) return AclEdit;
-
-        let effectiveAcl = AclPrivate;
-        const HierarchyMapping = new Map<symbol, number>([
+        HierarchyMapping = HierarchyMapping || new Map<symbol, number>([
             [AclPrivate, 0],
             [AclReadonly, 1],
             [AclAddonly, 2],
@@ -188,12 +191,13 @@ function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): s
             [AclAdmin, 4]
         ]);
 
-        for (const [key, value] of Object.entries(target[AclSym])) {
+        let effectiveAcl = AclPrivate;
+        for (const [key, value] of Object.entries(targetAcls)) {
             // there are issues with storing fields with . in the name, so they are replaced with _ during creation
             // as a result we need to restore them again during this comparison.
             const entity = denormalizeEmail(key.substring(4)); // an individual or a group
-            if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) {
-                if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+            if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
+                if (SnappingManager.GetCachedGroupByName(entity) || userChecked === entity) {
                     effectiveAcl = value as symbol;
                     if (effectiveAcl === AclAdmin) return effectiveAcl;
                 }
@@ -201,8 +205,8 @@ function getEffectiveAcl(target: any, playgroundProp: boolean, user?: string): s
         }
 
         // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document
-        const override = target[AclSym]["acl-Override"];
-        if (override !== AclUnset && override !== undefined) effectiveAcl = target[AclSym]["acl-Override"];
+        const override = targetAcls["acl-Override"];
+        if (override !== AclUnset && override !== undefined) effectiveAcl = override;
 
         // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl)
         return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl;
@@ -282,7 +286,7 @@ const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHe
     "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
 export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
     let prop = in_prop;
-    const effectiveAcl = GetEffectiveAcl(target, in_prop);
+    const effectiveAcl = getPropAcl(target, prop);
     if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin) return true;
 
     // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
-- 
cgit v1.2.3-70-g09d2

