From df25be868115b378e208fc729b75d606be279164 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 1 Oct 2020 10:15:26 -0400 Subject: changed sharing to use __ to replace .'s in emails to avoid problems with storing keys in mongo --- src/fields/util.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/fields') diff --git a/src/fields/util.ts b/src/fields/util.ts index f0ff2dad4..6c92aef81 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -116,6 +116,13 @@ export function OVERRIDE_acl(val: boolean) { _overrideAcl = val; } +export function normalizeEmail(email: string) { + return email.replace('.', '__'); +} +export function denormalizeEmail(email: string) { + return email.replace('__', '.'); +} + // playground mode allows the user to add/delete documents or make layout changes without them saving to the server // let playgroundMode = false; @@ -184,7 +191,7 @@ export function GetEffectiveAcl(target: any, in_prop?: string | symbol | number, for (const [key, value] of Object.entries(target[AclSym])) { // 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 = key.substring(4).replace('_', '.'); // an individual or a group + const entity = denormalizeEmail(key.substring(4)); // an individual or a group if (currentUserGroups.includes(entity) || userChecked === entity) { if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) { effectiveAcl = value as symbol; -- cgit v1.2.3-70-g09d2 From 6b34d26af04f3325e0f0368668b2c90f1a2ce855 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 1 Oct 2020 10:27:13 -0400 Subject: fixed clearing out doc data when permissions are lost. includes CurrentEmailNormalized from previous commit --- src/fields/Doc.ts | 74 +++++++++++++++++++++---------------------------------- 1 file changed, 28 insertions(+), 46 deletions(-) (limited to 'src/fields') diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 48fd831d6..59be3a27c 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -19,42 +19,30 @@ import { DateField } from "./DateField"; import { listSpec } from "./Schema"; import { ComputedField, ScriptField } from "./ScriptField"; import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; -import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions } from "./util"; +import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction, GetEffectiveAcl, SharingPermissions, normalizeEmail } from "./util"; import { LinkManager } from "../client/util/LinkManager"; import JSZip = require("jszip"); import { saveAs } from "file-saver"; import { CollectionDockingView } from "../client/views/collections/CollectionDockingView"; import { SelectionManager } from "../client/util/SelectionManager"; +import { CurrentUserUtils } from "../client/util/CurrentUserUtils"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { const onDelegate = Object.keys(doc).includes(key); - const field = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - if (Field.IsField(field)) { - return (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); - } - return ""; + return !Field.IsField(field) ? "" : (onDelegate ? "=" : "") + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field)); } export function toScriptString(field: Field): string { - if (typeof field === "string") { - return `"${field}"`; - } else if (typeof field === "number" || typeof field === "boolean") { - return String(field); - } else { - return field[ToScriptString](); - } + if (typeof field === "string") return `"${field}"`; + if (typeof field === "number" || typeof field === "boolean") return String(field); + return field[ToScriptString](); } export function toString(field: Field): string { - if (typeof field === "string") { - return field; - } else if (typeof field === "number" || typeof field === "boolean") { - return String(field); - } else if (field instanceof ObjectField) { - return field[ToString](); - } else if (field instanceof RefField) { - return field[ToString](); - } + if (typeof field === "string") return field; + if (typeof field === "number" || typeof field === "boolean") return String(field); + if (field instanceof ObjectField) return field[ToString](); + if (field instanceof RefField) return field[ToString](); return ""; } export function IsField(field: any): field is Field; @@ -86,16 +74,10 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); } -export async function DocCastAsync(field: FieldResult): Promise> { - return Cast(field, Doc); -} +export async function DocCastAsync(field: FieldResult): Promise> { return Cast(field, Doc); } -export function DocListCast(field: FieldResult): Doc[] { - return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; -} -export function DocListCastOrNull(field: FieldResult) { - return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; -} +export function DocListCast(field: FieldResult) { return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; } +export function DocListCastOrNull(field: FieldResult) { return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; } export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); @@ -128,10 +110,6 @@ export function fetchProto(doc: Doc) { if (Object.keys(permissions).length) doc[AclSym] = permissions; - if (GetEffectiveAcl(doc) === AclPrivate) { - runInAction(() => doc[FieldsSym](true)); - } - if (doc.proto instanceof Promise) { doc.proto.then(fetchProto); return doc.proto; @@ -193,17 +171,11 @@ export class Doc extends RefField { } } private get __fieldKeys() { return this.___fieldKeys; } - private set __fieldKeys(value) { - this.___fieldKeys = value; - } - - @observable - private ___fields: any = {}; + private set __fieldKeys(value) { this.___fieldKeys = value; } - @observable - private ___fieldKeys: any = {}; - @observable - public [AclSym]: { [key: string]: symbol }; + @observable private ___fields: any = {}; + @observable private ___fieldKeys: any = {}; + @observable public [AclSym]: { [key: string]: symbol }; private [UpdatingFromServer]: boolean = false; @@ -213,7 +185,11 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; - public [FieldsSym](clear?: boolean) { return clear ? this.___fields = this.___fieldKeys = {} : this.___fields; } + public [FieldsSym](clear?: boolean) { + const self = this[SelfProxy]; + runInAction(() => clear && Array.from(Object.keys(self)).forEach(key => delete self[key])); + return this.___fields; + } public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); public [ToScriptString] = () => `DOC-"${this[Self][Id]}"-`; @@ -242,6 +218,7 @@ export class Doc extends RefField { private [CachedUpdates]: { [key: string]: () => void | Promise } = {}; public static CurrentUserEmail: string = ""; + public static get CurrentUserEmailNormalized() { return normalizeEmail(Doc.CurrentUserEmail); } public async [HandleUpdate](diff: any) { const set = diff.$set; const sameAuthor = this.author === Doc.CurrentUserEmail; @@ -263,6 +240,11 @@ export class Doc extends RefField { if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) { DocServer.GetRefField(this[Id], true); } + if (prev !== AclPrivate && GetEffectiveAcl(this) === AclPrivate) { + this[UpdatingFromServer] = true; + this[FieldsSym](true); + this[UpdatingFromServer] = false; + } }; if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; -- cgit v1.2.3-70-g09d2 From 266590937d3fda5dd96729edaa7c9bfac42370d9 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 1 Oct 2020 20:31:39 -0400 Subject: major performance fix by adding a SharingDocument to the user's DB account so that inquiring the users' UserDoc (and everything it referecens) is no longer necessary. --- src/client/DocServer.ts | 10 ++++++ src/client/util/CurrentUserUtils.ts | 39 +++++++++++++--------- src/client/util/GroupManager.tsx | 12 +++---- src/client/util/SettingsManager.tsx | 4 +-- src/client/util/SharingManager.tsx | 37 +++++++++----------- src/client/views/GlobalKeyHandler.ts | 3 +- src/client/views/nodes/DocumentView.tsx | 2 +- src/fields/Doc.ts | 2 ++ src/mobile/AudioUpload.tsx | 2 +- src/mobile/ImageUpload.tsx | 4 +-- src/mobile/MobileInterface.tsx | 8 ++--- src/server/ApiManagers/UserManager.ts | 14 +++++--- src/server/GarbageCollector.ts | 2 +- src/server/authentication/AuthenticationManager.ts | 3 +- src/server/authentication/DashUserModel.ts | 4 ++- 15 files changed, 82 insertions(+), 64 deletions(-) (limited to 'src/fields') diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index d7dfa4498..9683eab45 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -9,6 +9,7 @@ import { GestureOverlay } from './views/GestureOverlay'; import MobileInkOverlay from '../mobile/MobileInkOverlay'; import { runInAction } from 'mobx'; import { ObjectField } from '../fields/ObjectField'; +import { StrCast } from '../fields/Types'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -25,6 +26,15 @@ import { ObjectField } from '../fields/ObjectField'; */ export namespace DocServer { let _cache: { [id: string]: RefField | Promise> } = {}; + + export function PRINT_CACHE() { + const strings: string[] = []; + Array.from(Object.keys(_cache)).forEach(key => { + const doc = _cache[key]; + if (doc instanceof Doc) strings.push(StrCast(doc.author) + " " + StrCast(doc.title) + " " + StrCast(Doc.GetT(doc, "title", "string", true))); + }); + strings.sort().forEach((str, i) => console.log(i.toString() + " " + str)); + } export let _socket: SocketIOClient.Socket; // this client's distinct GUID created at initialization let GUID: string; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index d45a2c90a..c00ad2334 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -404,7 +404,7 @@ export class CurrentUserUtils { const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { title: "header", version: headerViewVersion, target: doc, _height: 70, _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, cloneFieldFilter: new List(["system"]) }, "header"); // text needs to be a space to allow templateText to be created headerTemplate[DataSym].layout = "
" + - " " + + " " + " " + "
"; (headerTemplate.proto as Doc).isTemplateDoc = makeTemplate(headerTemplate.proto as Doc, true, "headerView"); @@ -513,10 +513,7 @@ export class CurrentUserUtils { return doc.myItemCreators as Doc; } - static menuBtnDescriptions(doc: Doc): { - title: string, target: Doc, icon: string, click: string, watchedDocuments?: Doc - }[] { - this.setupSharingSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing + static async menuBtnDescriptions(doc: Doc) { return [ { title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' }, { title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' }, @@ -540,9 +537,10 @@ export class CurrentUserUtils { })) as any as Doc; } } - static setupMenuPanel(doc: Doc) { + static async setupMenuPanel(doc: Doc, sharingDocumentId: string) { if (doc.menuStack === undefined) { - const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, target, icon, click, watchedDocuments }) => + await this.setupSharingSidebar(doc, sharingDocumentId); // 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, iconShape: "square", @@ -874,9 +872,18 @@ export class CurrentUserUtils { } // Sharing sidebar is where shared documents are contained - static setupSharingSidebar(doc: Doc) { + static async setupSharingSidebar(doc: Doc, sharingDocumentId: string) { if (doc.mySharedDocs === undefined) { - doc.mySharedDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, _showTitle: "title", ignoreClick: true, lockedPosition: true })); + let sharedDocs = await DocServer.GetRefField(sharingDocumentId); + if (!sharedDocs) { + sharedDocs = Docs.Create.StackingDocument([], { + title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "none", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, _showTitle: "title", ignoreClick: true, lockedPosition: true + }, sharingDocumentId); + } + if (sharedDocs instanceof Doc) { + sharedDocs.userColor = sharedDocs.userColor || "#12121233"; + } + doc.mySharedDocs = new PrefetchProxy(sharedDocs); } } @@ -943,11 +950,10 @@ export class CurrentUserUtils { return doc.clickFuncs as Doc; } - static async updateUserDocument(doc: Doc) { + static async updateUserDocument(doc: Doc, sharingDocumentId: string) { doc.system = true; doc.noviceMode = doc.noviceMode === undefined ? "true" : doc.noviceMode; doc.title = Doc.CurrentUserEmail; - doc.userColor = doc.userColor || "#12121233"; doc._raiseWhenDragged = true; doc.activeInkPen = doc; doc.activeInkColor = StrCast(doc.activeInkColor, "rgb(0, 0, 0)"); @@ -976,7 +982,7 @@ 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 - this.setupMenuPanel(doc); + await this.setupMenuPanel(doc, sharingDocumentId); doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument(); doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument(); doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument(); @@ -1013,10 +1019,11 @@ export class CurrentUserUtils { public static async loadUserDocument({ id, email }: { id: string, email: string }) { this.curr_id = id; Doc.CurrentUserEmail = email; - await rp.get(Utils.prepend("/getUserDocumentId")).then(id => { - if (id && id !== "guest") { - return DocServer.GetRefField(id).then(async field => - Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(id, true)))); + await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => { + const { userDocumentId, sharingDocumentId } = JSON.parse(ids) as any; + if (userDocumentId !== "guest") { + return DocServer.GetRefField(userDocumentId).then(async field => + Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(userDocumentId, true), sharingDocumentId))); } 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 cb512bca8..70ea48ab8 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -64,14 +64,10 @@ export class GroupManager extends React.Component<{}> { const userList = await RequestPromise.get(Utils.prepend("/getUsers")); const raw = JSON.parse(userList) as User[]; const evaluating = raw.map(async user => { - const userDocument = await DocServer.GetRefField(user.userDocumentId); - if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument.mySharedDocs, Doc); - runInAction(() => { - if (notificationDoc instanceof Doc) { - this.users.push(user.email); - } - }); + const userSharingDocument = await DocServer.GetRefField(user.sharingDocumentId); + if (userSharingDocument instanceof Doc) { + const notificationDoc = await Cast(userSharingDocument.mySharedDocs, Doc, null); + runInAction(() => notificationDoc && this.users.push(user.email)); } }); return Promise.all(evaluating).then(() => this.populating = false); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index cd01fea5a..9934f26d3 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -56,7 +56,7 @@ export class SettingsManager extends React.Component<{}> { @undoBatch changeFontFamily = action((e: React.ChangeEvent) => Doc.UserDoc().fontFamily = (e.currentTarget as any).value); @undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value); @undoBatch switchActiveBackgroundColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex)); - @undoBatch switchUserColor = action((color: ColorState) => Doc.UserDoc().userColor = String(color.hex)); + @undoBatch switchUserColor = action((color: ColorState) => Doc.SharingDoc().userColor = String(color.hex)); @undoBatch playgroundModeToggle = action(() => { this.playgroundMode = !this.playgroundMode; @@ -205,6 +205,6 @@ export class SettingsManager extends React.Component<{}> { isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} - dialogueBoxStyle={{ width: "600px", background: Cast(Doc.UserDoc().userColor, "string", null) }} />; + dialogueBoxStyle={{ width: "600px", background: Cast(Doc.SharingDoc().userColor, "string", null) }} />; } } \ No newline at end of file diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 984583ed5..0cf6cc87c 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -25,7 +25,7 @@ import { SearchBox } from "../views/search/SearchBox"; export interface User { email: string; - userDocumentId: string; + sharingDocumentId: string; } /** @@ -50,12 +50,11 @@ const storage = "data"; * A user who also has a notificationDoc. */ interface ValidatedUser { - user: User; - notificationDoc: Doc; - userColor: string; + user: User; // database minimal info to identify / communicate with a user (email, sharing doc id) + sharingDoc: Doc; // document to share/message another user + userColor: string; // stored on the sharinDoc, extracted for convenience? } - @observer export class SharingManager extends React.Component<{}> { public static Instance: SharingManager; @@ -119,7 +118,7 @@ export class SharingManager extends React.Component<{}> { } /** - * Populates the list of validated users (this.users) by adding registered users which have a mySharedDocs. + * Populates the list of validated users (this.users) by adding registered users which have a sharingDocument. */ populateUsers = async () => { if (!this.populating) { @@ -130,15 +129,9 @@ export class SharingManager extends React.Component<{}> { const evaluating = raw.map(async user => { const isCandidate = user.email !== Doc.CurrentUserEmail; if (isCandidate) { - const userDocument = await DocServer.GetRefField(user.userDocumentId); - if (userDocument instanceof Doc) { - const notificationDoc = await Cast(userDocument.mySharedDocs, Doc); - const userColor = StrCast(userDocument.userColor); - runInAction(() => { - if (notificationDoc instanceof Doc) { - this.users.push({ user, notificationDoc, userColor }); - } - }); + const userSharingDoc = await DocServer.GetRefField(user.sharingDocumentId); + if (userSharingDoc instanceof Doc) { + runInAction(() => this.users.push({ user, sharingDoc: userSharingDoc, userColor: StrCast(userSharingDoc.userColor) })); } } }); @@ -170,7 +163,7 @@ export class SharingManager extends React.Component<{}> { // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List).push(doc) : group.docsShared = new List([doc]); - users.forEach(({ user, notificationDoc }) => { + users.forEach(({ user, sharingDoc: notificationDoc }) => { if (permission !== SharingPermissions.None) Doc.IndexOf(doc, DocListCast(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, doc); // add the doc to the notificationDoc if it hasn't already been added else GetEffectiveAcl(doc, undefined, user.email) === AclPrivate && Doc.IndexOf((doc.aliasOf as Doc || doc), DocListCast(notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(notificationDoc, storage, (doc.aliasOf as Doc || doc)); // remove the doc from the list if it already exists }); @@ -185,7 +178,7 @@ export class SharingManager extends React.Component<{}> { */ 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.notificationDoc[storage])) === -1 && Doc.AddDocToList(user.notificationDoc, storage, doc)); + if (group.docsShared) DocListCast(group.docsShared).forEach(doc => Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) === -1 && Doc.AddDocToList(user.sharingDoc, storage, doc)); } /** @@ -216,7 +209,7 @@ export class SharingManager extends React.Component<{}> { if (group.docsShared) { DocListCast(group.docsShared).forEach(doc => { - Doc.IndexOf(doc, DocListCast(user.notificationDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.notificationDoc, storage, doc); // remove the doc only if it is in the list + Doc.IndexOf(doc, DocListCast(user.sharingDoc[storage])) !== -1 && Doc.RemoveDocFromList(user.sharingDoc, storage, doc); // remove the doc only if it is in the list }); } } @@ -235,7 +228,7 @@ export class SharingManager extends React.Component<{}> { const members: string[] = JSON.parse(StrCast(group.members)); const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email)); - users.forEach(({ notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc)); + users.forEach(({ sharingDoc: notificationDoc }) => Doc.RemoveDocFromList(notificationDoc, storage, doc)); }); } } @@ -244,7 +237,7 @@ export class SharingManager extends React.Component<{}> { * Shares the document with a user. */ setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => { - const { user, notificationDoc } = recipient; + const { user, sharingDoc: notificationDoc } = recipient; const target = targetDoc || this.targetDoc!; const key = normalizeEmail(user.email); const acl = `acl-${key}`; @@ -456,7 +449,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, notificationDoc, 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, sharingDoc: notificationDoc, 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-"; @@ -472,7 +465,7 @@ export class SharingManager extends React.Component<{}> { diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 1b2395423..2ea1c464f 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -55,7 +55,8 @@ export class KeyManager { }); public handle = action(async (e: KeyboardEvent) => { - if (e.key.toLowerCase() === "shift") KeyManager.Instance.ShiftPressed = true; + if (e.key.toLowerCase() === "shift" && e.ctrlKey && e.altKey) KeyManager.Instance.ShiftPressed = true; + if (!Doc.UserDoc().noviceMode && e.key.toLocaleLowerCase() === "shift") DocServer.PRINT_CACHE(); const keyname = e.key && e.key.toLowerCase(); this.handleGreedy(keyname); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5f99f27b1..ddcf7f6f4 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1049,7 +1049,7 @@ export class DocumentView extends DocComponent(Docu const titleView = (!this.ShowTitle ? (null) :
users.user.email === this.dataDoc.author)?.userColor || (this.rootDoc.type === DocumentType.RTF ? StrCast(Doc.UserDoc().userColor) : "rgba(0,0,0,0.4)"), + background: SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.userColor || (this.rootDoc.type === DocumentType.RTF ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)"), pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : undefined, }}> manager._searchQuery = query); } export function UserDoc(): Doc { return manager._user_doc; } + export function SharingDoc(): Doc { return Cast(Doc.UserDoc().mySharedDocs, 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/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx index 36663c85f..ebc8bc8a7 100644 --- a/src/mobile/AudioUpload.tsx +++ b/src/mobile/AudioUpload.tsx @@ -53,7 +53,7 @@ export class AudioUpload extends React.Component { * Pushing the audio doc onto Dash Web through the right side bar */ uploadAudio = () => { - const audioRightSidebar = Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc; + const audioRightSidebar = Cast(Doc.SharingDoc(), Doc, null); const audioDoc = this._audioCol; const data = Cast(audioRightSidebar.data, listSpec(Doc)); for (let i = 1; i < 8; i++) { diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 1ee4c7815..65f9e40ff 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -63,11 +63,11 @@ export class Uploader extends React.Component { doc = Docs.Create.ImageDocument(path, { _nativeWidth: defaultNativeImageDim, _width: 400, title: name }); } this.setOpacity(4, "1"); // Slab 4 - const res = await rp.get(Utils.prepend("/getUserDocumentId")); + const res = await rp.get(Utils.prepend("/getUserDocumentIds")); if (!res) { throw new Error("No user id returned"); } - const field = await DocServer.GetRefField(res); + const field = await DocServer.GetRefField(JSON.parse(res).userDocumentId); let pending: Opt; if (field instanceof Doc) { pending = col; diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 72914e62b..a42d85b56 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -440,7 +440,7 @@ export class MobileInterface extends React.Component { // DocButton that uses UndoManager and handles the opacity change if CanUndo is true @computed get undo() { if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && - this._activeDoc !== Doc.UserDoc().mySharedDocs && this._activeDoc.title !== "WORKSPACES") { + this._activeDoc !== Doc.SharingDoc() && this._activeDoc.title !== "WORKSPACES") { return (
; } @@ -632,7 +632,7 @@ export class MobileInterface extends React.Component { */ @action switchToMobileUploads = () => { - const mobileUpload = Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc; + const mobileUpload = Cast(Doc.SharingDoc(), Doc) as Doc; this.switchCurrentView(mobileUpload); this._homeMenu = false; } diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index 0d1d8f218..c9ffaff4c 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -19,16 +19,22 @@ export default class UserManager extends ApiManager { method: Method.GET, subscription: "/getUsers", secureHandler: async ({ res }) => { - const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users"); + const cursor = await Database.Instance.query({}, { email: 1, sharingDocumentId: 1 }, "users"); const results = await cursor.toArray(); - res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId }))); + res.send(results.map(user => ({ email: user.email, sharingDocumentId: user.sharingDocumentId }))); } }); register({ method: Method.GET, - subscription: "/getUserDocumentId", - secureHandler: ({ res, user }) => res.send(user.userDocumentId) + subscription: "/getUserDocumentIds", + secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, sharingDocumentId: user.sharingDocumentId }) + }); + + register({ + method: Method.GET, + subscription: "/getSharingDocumentId", + secureHandler: ({ res, user }) => res.send(user.sharingDocumentId) }); register({ diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index a9a3b0481..6bd0e5163 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); + const ids: string[] = [...users.map(user => user.userDocumentId), ...users.map(user => user.sharingDocumentId)]]; const visited = new Set(); const files: { [name: string]: string[] } = {}; diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts index 00f1fe44e..36363e3cf 100644 --- a/src/server/authentication/AuthenticationManager.ts +++ b/src/server/authentication/AuthenticationManager.ts @@ -47,7 +47,8 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { const model = { email, password, - userDocumentId: Utils.GenerateGuid() + userDocumentId: Utils.GenerateGuid(), + sharingDocumentId: Utils.GenerateGuid() } as Partial; const user = new User(model); diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index 51d920a8f..0bdc25644 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -10,6 +10,7 @@ export type DashUserModel = mongoose.Document & { passwordResetExpires?: Date, userDocumentId: string; + sharingDocumentId: string; profile: { name: string, @@ -35,7 +36,8 @@ const userSchema = new mongoose.Schema({ passwordResetToken: String, passwordResetExpires: Date, - userDocumentId: String, + 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 facebook: String, twitter: String, -- cgit v1.2.3-70-g09d2 From ae10a9c7e6188236023f790c1ff6abb0e211311e Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 1 Oct 2020 20:49:31 -0400 Subject: from last --- src/client/util/CurrentUserUtils.ts | 2 +- src/fields/Doc.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/fields') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index c00ad2334..add595cd1 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1023,7 +1023,7 @@ export class CurrentUserUtils { const { userDocumentId, sharingDocumentId } = JSON.parse(ids) as any; if (userDocumentId !== "guest") { return DocServer.GetRefField(userDocumentId).then(async field => - Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(userDocumentId, true), sharingDocumentId))); + await this.updateUserDocument(Doc.SetUserDoc(field instanceof Doc ? field : new Doc(userDocumentId, true)), sharingDocumentId)); } else { throw new Error("There should be a user id! Why does Dash think there isn't one?"); } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index d9c641973..a8a5ba9bd 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -888,7 +888,7 @@ export namespace Doc { export function SetSelectedTool(tool: InkTool) { Doc.UserDoc().activeInkTool = tool; } export function GetSelectedTool(): InkTool { return StrCast(Doc.UserDoc().activeInkTool, InkTool.None) as InkTool; } - export function SetUserDoc(doc: Doc) { manager._user_doc = doc; } + export function SetUserDoc(doc: Doc) { return (manager._user_doc = doc); } export function IsSearchMatch(doc: Doc) { return computedFn(function IsSearchMatch(doc: Doc) { -- cgit v1.2.3-70-g09d2 From 1dee63242684f02543cf7667b53baa00d10ab6c1 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 2 Oct 2020 00:34:30 -0400 Subject: fixed email (de)normalization. fixed sharing to hide objects whose permissions have been revoked --- src/client/views/collections/CollectionSubView.tsx | 7 +++++-- src/client/views/nodes/FontIconBox.tsx | 2 +- src/fields/util.ts | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) (limited to 'src/fields') diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 649b8c175..fa80c8062 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,6 +1,6 @@ import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; import CursorField from "../../../fields/CursorField"; -import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc"; +import { Doc, Opt, Field, DocListCast, AclPrivate } from "../../../fields/Doc"; import { Id, ToString } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; @@ -101,7 +101,10 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: get childLayoutPairs(): { layout: Doc; data: Doc; }[] { const { Document, DataDoc } = this.props; - const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? DataDoc : undefined, doc)).filter(pair => pair.layout); + const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? DataDoc : undefined, doc)). + filter(pair => { // filter out any documents that have a proto that we don't have permissions to (which we determine by not having any keys + return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && Object.keys(pair.layout.proto).length)); + }); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } get childDocList() { diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx index 6e96513c7..156256fe5 100644 --- a/src/client/views/nodes/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox.tsx @@ -105,7 +105,7 @@ export class FontIconBadge extends React.Component { render() { if (!(this.props.collection instanceof Doc)) return (null); - const length = DocListCast(this.props.collection.data).length; + const length = DocListCast(this.props.collection.data).filter(d => Object.keys(d).length).length; // filter out any documents that we can't read return
0 ? { "display": "initial" } : { "display": "none" }} onPointerDown={this.onPointerDown} > diff --git a/src/fields/util.ts b/src/fields/util.ts index 6c92aef81..4da9fce74 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -117,10 +117,10 @@ export function OVERRIDE_acl(val: boolean) { } export function normalizeEmail(email: string) { - return email.replace('.', '__'); + return email.replace(/\./g, '__'); } export function denormalizeEmail(email: string) { - return email.replace('__', '.'); + return email.replace(/__/g, '.'); } // playground mode allows the user to add/delete documents or make layout changes without them saving to the server -- cgit v1.2.3-70-g09d2