aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgeireann <60007097+geireann@users.noreply.github.com>2020-10-02 20:52:10 +0800
committergeireann <60007097+geireann@users.noreply.github.com>2020-10-02 20:52:10 +0800
commit805bf106c85f693f6e6d15c06cacd5e16079c707 (patch)
tree872f84dee5ffcae04fbfe128941c0429f3aee562
parent3b14058df2cf9cb444836a6b1fea92835eb51761 (diff)
parent1dee63242684f02543cf7667b53baa00d10ab6c1 (diff)
Merge branch 'master' into presentation_v1
-rw-r--r--src/client/DocServer.ts10
-rw-r--r--src/client/documents/Documents.ts12
-rw-r--r--src/client/util/CurrentUserUtils.ts56
-rw-r--r--src/client/util/GroupManager.tsx12
-rw-r--r--src/client/util/SearchUtil.ts2
-rw-r--r--src/client/util/SettingsManager.tsx4
-rw-r--r--src/client/util/SharingManager.tsx68
-rw-r--r--src/client/views/DocComponent.tsx4
-rw-r--r--src/client/views/GlobalKeyHandler.ts3
-rw-r--r--src/client/views/MainView.tsx69
-rw-r--r--src/client/views/PropertiesView.tsx23
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx2
-rw-r--r--src/client/views/collections/CollectionMenu.tsx7
-rw-r--r--src/client/views/collections/CollectionSubView.tsx7
-rw-r--r--src/client/views/collections/CollectionView.tsx4
-rw-r--r--src/client/views/nodes/DocumentView.tsx2
-rw-r--r--src/client/views/nodes/FontIconBox.tsx2
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts8
-rw-r--r--src/fields/Doc.ts78
-rw-r--r--src/fields/util.ts9
-rw-r--r--src/mobile/AudioUpload.tsx2
-rw-r--r--src/mobile/ImageUpload.tsx4
-rw-r--r--src/mobile/MobileInterface.tsx8
-rw-r--r--src/server/ApiManagers/UserManager.ts14
-rw-r--r--src/server/GarbageCollector.ts2
-rw-r--r--src/server/authentication/AuthenticationManager.ts3
-rw-r--r--src/server/authentication/DashUserModel.ts4
27 files changed, 215 insertions, 204 deletions
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<Opt<RefField>> } = {};
+
+ 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/documents/Documents.ts b/src/client/documents/Documents.ts
index 47fa7067d..7ee8267f8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -574,7 +574,7 @@ export namespace Docs {
* only when creating a DockDocument from the current user's already existing
* main document.
*/
- export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data") {
+ export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data", protoId?: string) {
const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys, "^_");
protoProps.system = delegateProps.system;
@@ -590,7 +590,7 @@ export namespace Docs {
protoProps.isPrototype = true;
- const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey);
+ const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey, protoId);
const viewDoc = Doc.MakeDelegate(dataDoc, delegId);
// so that the list of annotations is already initialised, prevents issues in addonly.
@@ -620,8 +620,8 @@ export namespace Docs {
* @param options initial values to apply to this new delegate
* @param value the data to store in this new delegate
*/
- function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D, fieldKey: string = "data") {
- const deleg = Doc.MakeDelegate(proto);
+ function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D, fieldKey: string = "data", id: string | undefined = undefined) {
+ const deleg = Doc.MakeDelegate(proto, id);
if (value !== undefined) {
deleg[fieldKey] = value;
}
@@ -807,8 +807,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", dontRegisterChildViews: true, ...options, _viewType: CollectionViewType.Tree }, id);
}
- export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Stacking }, id);
+ export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId);
}
export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 7cc35d67a..ec550c15a 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -30,6 +30,7 @@ import { Scripting } from "./Scripting";
import { SearchUtil } from "./SearchUtil";
import { SelectionManager } from "./SelectionManager";
import { UndoManager } from "./UndoManager";
+import { SharingPermissions } from "../../fields/util";
const headerViewVersion = "0.1";
@@ -404,7 +405,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<string>(["system"]) }, "header"); // text needs to be a space to allow templateText to be created
headerTemplate[DataSym].layout =
"<div style={'height:100%'}>" +
- " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.userColor}' />" +
+ " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" +
" <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" +
"</div>";
(headerTemplate.proto as Doc).isTemplateDoc = makeTemplate(headerTemplate.proto as Doc, true, "headerView");
@@ -513,10 +514,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 +538,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",
@@ -566,7 +565,7 @@ export class CurrentUserUtils {
title: "menuItemPanel",
childDropAction: "alias",
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
- _backgroundColor: "black",
+ _backgroundColor: "black", ignoreClick: true,
_gridGap: 0,
_yMargin: 0,
_yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, lockedPosition: true, _chromeStatus: "disabled", system: true
@@ -729,7 +728,7 @@ export class CurrentUserUtils {
if (doc.myTools === undefined) {
const toolsStack = new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], {
- title: "My Tools", _width: 500, _yMargin: 20, lockedPosition: true, _chromeStatus: "disabled", forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true,
+ title: "My Tools", _width: 500, _yMargin: 20, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", forceActive: true, system: true, _stayInCollection: true, _hideContextMenu: true,
})) as any as Doc;
doc.myTools = toolsStack;
@@ -743,7 +742,7 @@ export class CurrentUserUtils {
doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Dashboards", _height: 400,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
}));
const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`);
@@ -759,7 +758,7 @@ export class CurrentUserUtils {
doc.myPresentations = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Presentations", _height: 100,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
}));
const newPresentations = ScriptField.MakeScript(`createNewPresentation()`);
@@ -777,7 +776,7 @@ export class CurrentUserUtils {
doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "Recently Closed", _height: 500,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
}));
const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`);
@@ -792,7 +791,7 @@ export class CurrentUserUtils {
doc.myFilter = new PrefetchProxy(Docs.Create.FilterDocument({
title: "FilterDoc", _height: 500,
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "none",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
}));
const clearAll = ScriptField.MakeScript(`getProto(self).data = new List([])`);
@@ -808,7 +807,7 @@ export class CurrentUserUtils {
doc.treeViewExpandedView = "fields";
doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], {
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, title: "My UserDoc",
- treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false,
+ treeViewTruncateTitleWidth: 150, treeViewPreventOpen: false, ignoreClick: true,
lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true, targetDropAction: "same", system: true
})) as any as Doc;
}
@@ -874,9 +873,20 @@ 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 + "outer");
+ 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 + "outer", sharingDocumentId);
+ (sharedDocs as Doc)["acl-Public"] = Doc.GetProto(sharedDocs as Doc)["acl-Public"] = SharingPermissions.Add;
+ }
+ if (sharedDocs instanceof Doc) {
+ sharedDocs.userColor = sharedDocs.userColor || "#12121233";
+ }
+ doc.mySharedDocs = new PrefetchProxy(sharedDocs);
}
}
@@ -943,11 +953,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 +985,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 +1022,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);
+ if (userDocumentId !== "guest") {
+ return DocServer.GetRefField(userDocumentId).then(async field =>
+ 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/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index cb512bca8..e81c95d83 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.data, Doc, null);
+ runInAction(() => notificationDoc && this.users.push(user.email));
}
});
return Promise.all(evaluating).then(() => this.populating = false);
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 08ad49dcc..79759a71d 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -44,7 +44,7 @@ export namespace SearchUtil {
const header = query.match(/_[atnb]?:/) ? replacedQuery : "DEFAULT:" + replacedQuery;
replacedQuery = `{!join from=id to=proto_i}* AND ${header}`;
}
- console.log("Q: " + replacedQuery + " fq: " + options.fq);
+ //console.log("Q: " + replacedQuery + " fq: " + options.fq);
const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } });
const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten);
if (!returnDocs) {
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 bcd7d4056..c67ec2db5 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -7,7 +7,7 @@ import * as RequestPromise from "request-promise";
import { AclAdmin, AclPrivate, DataSym, Doc, DocListCast, Opt, AclSym, AclAddonly, AclEdit, AclReadonly } from "../../fields/Doc";
import { List } from "../../fields/List";
import { Cast, StrCast } from "../../fields/Types";
-import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from "../../fields/util";
+import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx, normalizeEmail } from "../../fields/util";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { CollectionView } from "../views/collections/CollectionView";
@@ -25,7 +25,7 @@ import { SearchBox } from "../views/search/SearchBox";
export interface User {
email: string;
- userDocumentId: string;
+ sharingDocumentId: string;
}
/**
@@ -47,20 +47,19 @@ const groupType = "!groupType/";
const storage = "data";
/**
- * A user who also has a notificationDoc.
+ * A user who also has a sharing doc.
*/
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;
@observable private isOpen = false; // whether the SharingManager modal is open or not
- @observable public users: ValidatedUser[] = []; // the list of users with notificationDocs
+ @observable public users: ValidatedUser[] = []; // the list of users with sharing docs
@observable private targetDoc: Doc | undefined; // the document being shared
@observable private targetDocView: DocumentView | undefined; // the DocumentView of the document being shared
// @observable private copied = false;
@@ -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) }));
}
}
});
@@ -154,13 +147,13 @@ export class SharingManager extends React.Component<{}> {
setInternalGroupSharing = (group: Doc | { groupName: string }, permission: string, targetDoc?: Doc) => {
const target = targetDoc || this.targetDoc!;
- const key = StrCast(group.groupName).replace(".", "_");
+ const key = normalizeEmail(StrCast(group.groupName));
const acl = `acl-${key}`;
const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document);
docs.forEach(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`acl-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, doc);
+ doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc);
distributeAcls(acl, permission as SharingPermissions, doc);
if (group instanceof Doc) {
@@ -170,9 +163,9 @@ 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<Doc>).push(doc) : group.docsShared = new List<Doc>([doc]);
- users.forEach(({ user, 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
+ 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
});
}
});
@@ -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 }) => Doc.RemoveDocFromList(sharingDoc, storage, doc));
});
}
}
@@ -244,19 +237,18 @@ 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 } = recipient;
const target = targetDoc || this.targetDoc!;
- const key = user.email.replace('.', '_');
- const acl = `acl-${key}`;
+ const acl = `acl-${normalizeEmail(user.email)}`;
+ const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`;
const docs = SelectionManager.SelectedDocuments().length < 2 ? [target] : SelectionManager.SelectedDocuments().map(docView => docView.props.Document);
-
docs.forEach(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`] && distributeAcls(`acl-${Doc.CurrentUserEmail.replace(".", "_")}`, SharingPermissions.Admin, doc);
+ 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(notificationDoc[storage])) === -1 && Doc.AddDocToList(notificationDoc, storage, doc);
- 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));
+ 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));
});
}
@@ -456,8 +448,8 @@ 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-${user.email.replace('.', '_')}`) : docs[0]?.author !== user.email).map(({ user, notificationDoc, userColor }) => {
- const userKey = `acl-${user.email.replace('.', '_')}`;
+ 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 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 +464,7 @@ export class SharingManager extends React.Component<{}> {
<select
className={"permissions-dropdown"}
value={permissions}
- onChange={e => this.setInternalSharing({ user, notificationDoc, userColor }, e.currentTarget.value)}
+ onChange={e => this.setInternalSharing({ user, sharingDoc: sharingDoc, userColor }, e.currentTarget.value)}
>
{this.sharingOptions(uniform)}
</select>
@@ -514,7 +506,7 @@ export class SharingManager extends React.Component<{}> {
<span className={"padding"}>Me</span>
<div className="edit-actions">
<div className={"permissions-dropdown"}>
- {targetDoc?.[`acl-${Doc.CurrentUserEmail.replace(".", "_")}`]}
+ {targetDoc?.[`acl-${Doc.CurrentUserEmailNormalized}`]}
</div>
</div>
</div>
@@ -523,7 +515,7 @@ 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-${StrCast(groupName).replace('.', '_')}`) : true);
+ 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 groupListContents = groupListMap.map(group => {
const groupKey = `acl-${StrCast(group.groupName)}`;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 893b74d75..b3fbe418b 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -7,7 +7,7 @@ import { InteractionUtils } from '../util/InteractionUtils';
import { List } from '../../fields/List';
import { DateField } from '../../fields/DateField';
import { ScriptField } from '../../fields/ScriptField';
-import { GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../fields/util';
+import { GetEffectiveAcl, SharingPermissions, distributeAcls, denormalizeEmail } from '../../fields/util';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
@@ -166,7 +166,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
if (this.props.Document[AclSym]) {
added.forEach(d => {
for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- if (d.author === key.substring(4).replace("_", ".") && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
+ if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
//else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true);
// else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
}
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/MainView.tsx b/src/client/views/MainView.tsx
index 5193c3c02..8e30eba2a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -312,39 +312,40 @@ export class MainView extends React.Component {
}
@computed get flyout() {
- return <div className={`mainView-libraryFlyout${this._flyoutWidth ? "" : "-out"}`} style={{ minWidth: this._flyoutWidth, width: this._flyoutWidth }} >
- <div className="mainView-contentArea" >
- <DocumentView
- Document={this._sidebarContent}
- DataDoc={undefined}
- LibraryPath={emptyPath}
- addDocument={undefined}
- addDocTab={this.addDocTabFunc}
- pinToPres={emptyFunction}
- rootSelected={returnTrue}
- removeDocument={returnFalse}
- onClick={undefined}
- ScreenToLocalTransform={this.mainContainerXf}
- ContentScaling={returnOne}
- PanelWidth={this.flyoutWidthFunc}
- PanelHeight={this.getContentsHeight}
- renderDepth={0}
- focus={emptyFunction}
- backgroundColor={this.defaultBackgroundColors}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- relative={true}
- forcedBackgroundColor={() => "lightgrey"}
- />
- </div>
- {this.docButtons}
- </div>;
+ return !this._flyoutWidth ? <div className={`mainView-libraryFlyout-out"}`} style={{ width: 0 }} /> :
+ <div className="mainView-libraryFlyout" style={{ minWidth: this._flyoutWidth, width: this._flyoutWidth }} >
+ <div className="mainView-contentArea" >
+ <DocumentView
+ Document={this._sidebarContent}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ addDocument={undefined}
+ addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
+ rootSelected={returnTrue}
+ removeDocument={returnFalse}
+ onClick={undefined}
+ ScreenToLocalTransform={this.mainContainerXf}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getContentsHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={this.defaultBackgroundColors}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ relative={true}
+ forcedBackgroundColor={() => "lightgrey"}
+ />
+ </div>
+ {this.docButtons}
+ </div>;
}
@computed get menuPanel() {
@@ -394,6 +395,8 @@ export class MainView extends React.Component {
SearchBox.Instance._searchFullDB = "My Stuff";
SearchBox.Instance.enter(undefined);
break;
+ case "Help":
+ break;
default:
this.expandFlyout(button);
}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 5cab7726e..f3241e8d9 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1,6 +1,7 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Checkbox, Tooltip } from "@material-ui/core";
+import { intersection } from "lodash";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { ColorState, SketchPicker } from "react-color";
@@ -9,8 +10,8 @@ import { Id } from "../../fields/FieldSymbols";
import { InkField } from "../../fields/InkField";
import { ComputedField } from "../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../fields/Types";
-import { GetEffectiveAcl, SharingPermissions } from "../../fields/util";
-import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnZero } from "../../Utils";
+import { GetEffectiveAcl, SharingPermissions, denormalizeEmail } from "../../fields/util";
+import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne } from "../../Utils";
import { DocumentType } from "../documents/DocumentTypes";
import { DocumentManager } from "../util/DocumentManager";
import { SelectionManager } from "../util/SelectionManager";
@@ -26,7 +27,6 @@ import { PresBox } from "./nodes/PresBox";
import { PropertiesButtons } from "./PropertiesButtons";
import { PropertiesDocContextSelector } from "./PropertiesDocContextSelector";
import "./PropertiesView.scss";
-import { intersection } from "lodash";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -409,7 +409,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
// DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => {
if (commonKeys.length) {
for (const key of commonKeys) {
- const name = key.substring(4).replace("_", ".");
+ const name = denormalizeEmail(key.substring(4));
const uniform = docs.every(doc => this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key]);
if (name !== Doc.CurrentUserEmail && name !== target.author && name !== "Public" && name !== "Override"/* && sidebarUsersDisplayed![name] !== false*/) {
tableEntries.push(this.sharingItem(name, showAdmin, uniform ? AclMap.get(this.layoutDocAcls ? target[AclSym][key] : target[DataSym][AclSym][key])! : "-multiple-"));
@@ -417,13 +417,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
}
- // if (Doc.UserDoc().sidebarUsersDisplayed) {
- // for (const [name, value] of Object.entries(sidebarUsersDisplayed!)) {
- // if (value === true && !this.selectedDoc![`acl-${name.substring(8).replace(".", "_")}`]) tableEntries.push(this.sharingItem(name.substring(8), effectiveAcl, SharingPermissions.None));
- // }
- // }
- // })
-
const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author);
// shifts the current user, owner, public to the top of the doc.
tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-"));
@@ -910,9 +903,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
/>
<div className="propertiesView-acls-checkbox-text">Layout</div>
</div>) : (null)}
- <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}>
- <FontAwesomeIcon icon="redo-alt" color="white" size="1x" />
- </button>
+ <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}>
+ <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}>
+ <FontAwesomeIcon icon="redo-alt" color="white" size="1x" />
+ </button>
+ </Tooltip>
</div>
{this.sharingTable}
</div>}
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 4f1ef6e61..4e30709a6 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -166,7 +166,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume
render() {
const index = NumCast(this.layoutDoc._itemIndex);
- const translateX = (1 - index) / this.childLayoutPairs.length * 100;
+ const translateX = 33 * (1 - index);
return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}>
<div className="carousel-wrapper" style={{ transform: `translateX(calc(${translateX}%` }}>
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 51d1d5559..09ff3bb0c 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -234,8 +234,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp
@undoBatch
viewChanged = (e: React.ChangeEvent) => {
+ const target = this.document !== Doc.UserDoc().sidebar ? this.document : this.document.proto as Doc;
//@ts-ignore
- this.document._viewType = e.target.selectedOptions[0].value;
+ target._viewType = e.target.selectedOptions[0].value;
}
commandChanged = (e: React.ChangeEvent) => {
@@ -893,14 +894,14 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu
if (docs instanceof Doc) {
const keys = Object.keys(docs).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 ||
key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 ||
- (key[0].toUpperCase() === key[0] && key.substring(0, 3) !== "acl" && key[0] !== "_"));
+ (key[0].toUpperCase() === key[0] && key[0] !== "_"));
return keys.filter(key => key.toLowerCase().indexOf(val) > -1);
} else {
const keys = new Set<string>();
docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
const noviceKeys = Array.from(keys).filter(key => key.indexOf("title") >= 0 || key.indexOf("author") >= 0 ||
key.indexOf("creationDate") >= 0 || key.indexOf("lastModified") >= 0 ||
- (key[0]?.toUpperCase() === key[0] && key.substring(0, 3) !== "acl" && key[0] !== "_"));
+ (key[0]?.toUpperCase() === key[0] && key[0] !== "_"));
return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1);
}
}
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<T, X>(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/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 522f46280..2bdc8e2f3 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -10,7 +10,7 @@ import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
-import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
+import { distributeAcls, GetEffectiveAcl, SharingPermissions, TraceMobx, normalizeEmail, denormalizeEmail } from '../../../fields/util';
import { returnFalse, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -143,7 +143,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
if (this.props.Document[AclSym]) {
added.forEach(d => {
for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- if (d.author === key.substring(4).replace("_", ".") && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
+ if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d, true);
//else if (this.props.Document[key] === SharingPermissions.Admin) distributeAcls(key, SharingPermissions.Add, d, true);
//else distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true);
}
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<DocumentViewProps, Document>(Docu
const titleView = (!this.ShowTitle ? (null) :
<div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} key="title" style={{
position: this.showOverlappingTitle ? "absolute" : "relative",
- background: SharingManager.Instance.users.find(users => 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,
}}>
<EditableView ref={this._titleRef}
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<FontIconBadgeProps> {
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 <div className="fontIconBadge-container" style={{ width: 15, height: 15, top: 12 }} ref={this._notifsRef}>
<div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}
onPointerDown={this.onPointerDown} >
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 921c0e128..72b91c955 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -3,7 +3,8 @@ import { NodeSelection, TextSelection } from "prosemirror-state";
import { DataSym, Doc } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
import { ComputedField } from "../../../../fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../../fields/Types";
+import { NumCast, StrCast } from "../../../../fields/Types";
+import { normalizeEmail } from "../../../../fields/util";
import { returnFalse, Utils } from "../../../../Utils";
import { DocServer } from "../../../DocServer";
import { Docs, DocUtils } from "../../../documents/Documents";
@@ -11,7 +12,6 @@ import { FormattedTextBox } from "./FormattedTextBox";
import { wrappingInputRule } from "./prosemirrorPatches";
import { RichTextMenu } from "./RichTextMenu";
import { schema } from "./schema_rts";
-import { List } from "../../../../fields/List";
export class RichTextRules {
public Document: Doc;
@@ -271,7 +271,7 @@ export class RichTextRules {
(state, match, start, end) => {
const fieldKey = match[1];
const rawdocid = match[3]?.substring(1);
- const docid = rawdocid ? (!rawdocid.includes("@") ? Doc.CurrentUserEmail + "@" + rawdocid : rawdocid).replace(".", "_") : undefined;
+ const docid = rawdocid ? (!rawdocid.includes("@") ? normalizeEmail(Doc.CurrentUserEmail) + "@" + rawdocid : rawdocid) : undefined;
const value = match[2]?.substring(1);
if (!fieldKey) {
const linkId = Utils.GenerateGuid();
@@ -304,7 +304,7 @@ export class RichTextRules {
const fieldKey = match[1] || "";
const fieldParam = match[2]?.replace("…", "...") || "";
const rawdocid = match[3]?.substring(1);
- const docid = rawdocid ? (!rawdocid.includes("@") ? Doc.CurrentUserEmail + "@" + rawdocid : rawdocid).replace(".", "_") : undefined;
+ const docid = rawdocid ? (!rawdocid.includes("@") ? normalizeEmail(Doc.CurrentUserEmail) + "@" + rawdocid : rawdocid) : undefined;
if (!fieldKey && !docid) return state.tr;
docid && DocServer.GetRefField(docid).then(docx => {
if (!(docx instanceof Doc && docx)) {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 48fd831d6..a8a5ba9bd 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<Opt<Doc>> {
- return Cast(field, Doc);
-}
+export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { 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<any> } = {};
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];
@@ -885,6 +867,7 @@ export namespace Doc {
export class DocData {
@observable _user_doc: Doc = undefined!;
+ @observable _sharing_doc: Doc = undefined!;
@observable _searchQuery: string = "";
}
@@ -901,10 +884,11 @@ export namespace Doc {
export function SearchQuery(): string { return manager._searchQuery; }
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 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) {
diff --git a/src/fields/util.ts b/src/fields/util.ts
index f0ff2dad4..4da9fce74 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(/\./g, '__');
+}
+export function denormalizeEmail(email: string) {
+ return email.replace(/__/g, '.');
+}
+
// 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;
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<ImageUploadProps> {
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<Doc>;
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 (
<div className="docButton"
style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanUndo() ? "1" : "0.4", }}
@@ -458,7 +458,7 @@ export class MobileInterface extends React.Component {
// DocButton that uses UndoManager and handles the opacity change if CanRedo is true
@computed get redo() {
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 (
<div className="docButton"
style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanRedo() ? "1" : "0.4", }}
@@ -609,7 +609,7 @@ export class MobileInterface extends React.Component {
// Returns the image upload pop up
@computed get uploadImage() {
- const doc = !this._homeMenu ? this._activeDoc : Cast(Doc.UserDoc().mySharedDocs, Doc) as Doc;
+ const doc = !this._homeMenu ? this._activeDoc : Cast(Doc.SharingDoc(), Doc) as Doc;
return <Uploader Document={doc} />;
}
@@ -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<string>();
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<DashUserModel>;
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,