diff --git a/.gitignore b/.gitignore
index 1c1d25826..df445813e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ npm-debug.log
stats.json
.DS_Store
fakes3/*
+.idea
diff --git a/app/components/Badge.js b/app/components/Badge.js
index 646fede8d..c4d3f3604 100644
--- a/app/components/Badge.js
+++ b/app/components/Badge.js
@@ -4,8 +4,8 @@ import styled from "styled-components";
const Badge = styled.span`
margin-left: 10px;
padding: 2px 6px 3px;
- background-color: ${({ primary, theme }) =>
- primary ? theme.primary : theme.textTertiary};
+ background-color: ${({ yellow, primary, theme }) =>
+ yellow ? theme.yellow : primary ? theme.primary : theme.textTertiary};
color: ${({ primary, theme }) => (primary ? theme.white : theme.background)};
border-radius: 4px;
font-size: 11px;
diff --git a/app/components/DocumentMeta.js b/app/components/DocumentMeta.js
index 4f2366b88..08eede428 100644
--- a/app/components/DocumentMeta.js
+++ b/app/components/DocumentMeta.js
@@ -52,6 +52,7 @@ function DocumentMeta({
archivedAt,
deletedAt,
isDraft,
+ lastViewedAt,
} = document;
// Prevent meta information from displaying if updatedBy is not available.
@@ -103,6 +104,17 @@ function DocumentMeta({
const collection = collections.get(document.collectionId);
const updatedByMe = auth.user && auth.user.id === updatedBy.id;
+ const timeSinceNow = () => {
+ if (!lastViewedAt)
+ return Never viewed;
+
+ return (
+
+ Viewed ago
+
+ );
+ };
+
return (
{updatedByMe ? "You" : updatedBy.name}
@@ -115,6 +127,7 @@ function DocumentMeta({
)}
+ • {timeSinceNow()}
{children}
);
diff --git a/app/components/DocumentPreview/DocumentPreview.js b/app/components/DocumentPreview/DocumentPreview.js
index 371f44aca..2cd050956 100644
--- a/app/components/DocumentPreview/DocumentPreview.js
+++ b/app/components/DocumentPreview/DocumentPreview.js
@@ -105,6 +105,7 @@ class DocumentPreview extends React.Component {
{document.isTemplate && showTemplate && (
Template
)}
+ {document.isNew && New}
{document.isTemplate &&
!document.isArchived &&
diff --git a/app/models/Document.js b/app/models/Document.js
index 7538fe927..91818e040 100644
--- a/app/models/Document.js
+++ b/app/models/Document.js
@@ -1,5 +1,6 @@
// @flow
import addDays from "date-fns/add_days";
+import differenceInDays from "date-fns/difference_in_days";
import invariant from "invariant";
import { action, computed, observable, set } from "mobx";
import parseTitle from "shared/utils/parseTitle";
@@ -7,6 +8,7 @@ import unescape from "shared/utils/unescape";
import DocumentsStore from "stores/DocumentsStore";
import BaseModel from "models/BaseModel";
import User from "models/User";
+import View from "./View";
type SaveOptions = {
publish?: boolean,
@@ -23,7 +25,7 @@ export default class Document extends BaseModel {
collaborators: User[];
collectionId: string;
- lastViewedAt: ?string;
+ @observable lastViewedAt: ?string;
createdAt: string;
createdBy: User;
updatedAt: string;
@@ -47,7 +49,7 @@ export default class Document extends BaseModel {
constructor(fields: Object, store: DocumentsStore) {
super(fields, store);
- if (this.isNew && this.isFromTemplate) {
+ if (this.isNewDocument && this.isFromTemplate) {
this.title = "";
}
}
@@ -72,6 +74,14 @@ export default class Document extends BaseModel {
return !!this.lastViewedAt && this.lastViewedAt < this.updatedAt;
}
+ @computed
+ get isNew(): boolean {
+ return (
+ !this.lastViewedAt &&
+ differenceInDays(new Date(), new Date(this.createdAt)) < 14
+ );
+ }
+
@computed
get isStarred(): boolean {
return !!this.store.starredIds.get(this.id);
@@ -112,7 +122,7 @@ export default class Document extends BaseModel {
}
@computed
- get isNew(): boolean {
+ get isNewDocument(): boolean {
return this.createdAt === this.updatedAt;
}
@@ -199,6 +209,11 @@ export default class Document extends BaseModel {
return this.store.rootStore.views.create({ documentId: this.id });
};
+ @action
+ updateLastViewed = (view: View) => {
+ this.lastViewedAt = view.lastViewedAt;
+ };
+
@action
templatize = async () => {
return this.store.templatize(this.id);
diff --git a/app/scenes/Document/components/MarkAsViewed.js b/app/scenes/Document/components/MarkAsViewed.js
index 8a12504ba..5c74f388e 100644
--- a/app/scenes/Document/components/MarkAsViewed.js
+++ b/app/scenes/Document/components/MarkAsViewed.js
@@ -15,9 +15,10 @@ class MarkAsViewed extends React.Component {
componentDidMount() {
const { document } = this.props;
- this.viewTimeout = setTimeout(() => {
+ this.viewTimeout = setTimeout(async () => {
if (document.publishedAt) {
- document.view();
+ const view = await document.view();
+ document.updateLastViewed(view);
}
}, MARK_AS_VIEWED_AFTER);
}
diff --git a/app/stores/BaseStore.js b/app/stores/BaseStore.js
index eab617e52..a8f97e929 100644
--- a/app/stores/BaseStore.js
+++ b/app/stores/BaseStore.js
@@ -114,7 +114,7 @@ export default class BaseStore {
}
@action
- async delete(item: T, options?: Object = {}) {
+ async delete(item: T, options: Object = {}) {
if (!this.actions.includes("delete")) {
throw new Error(`Cannot delete ${this.modelName}`);
}
@@ -132,7 +132,7 @@ export default class BaseStore {
}
@action
- async fetch(id: string, options?: Object = {}): Promise<*> {
+ async fetch(id: string, options: Object = {}): Promise<*> {
if (!this.actions.includes("info")) {
throw new Error(`Cannot fetch ${this.modelName}`);
}
diff --git a/app/stores/DocumentsStore.js b/app/stores/DocumentsStore.js
index ff1aa120e..1e9e1046e 100644
--- a/app/stores/DocumentsStore.js
+++ b/app/stores/DocumentsStore.js
@@ -399,7 +399,7 @@ export default class DocumentsStore extends BaseStore {
@action
fetch = async (
id: string,
- options?: FetchOptions = {}
+ options: FetchOptions = {}
): Promise => {
if (!options.prefetch) this.isFetching = true;
diff --git a/server/api/documents.js b/server/api/documents.js
index 6795d9164..6e760b2b4 100644
--- a/server/api/documents.js
+++ b/server/api/documents.js
@@ -98,10 +98,12 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
// add the users starred state to the response by default
const starredScope = { method: ["withStarred", user.id] };
const collectionScope = { method: ["withCollection", user.id] };
+ const viewScope = { method: ["withViews", user.id] };
const documents = await Document.scope(
"defaultScope",
starredScope,
- collectionScope
+ collectionScope,
+ viewScope
).findAll({
where,
order: [[sort, direction]],
@@ -137,10 +139,12 @@ router.post("documents.pinned", auth(), pagination(), async (ctx) => {
const starredScope = { method: ["withStarred", user.id] };
const collectionScope = { method: ["withCollection", user.id] };
+ const viewScope = { method: ["withViews", user.id] };
const documents = await Document.scope(
"defaultScope",
starredScope,
- collectionScope
+ collectionScope,
+ viewScope
).findAll({
where: {
teamId: user.teamId,
@@ -176,9 +180,11 @@ router.post("documents.archived", auth(), pagination(), async (ctx) => {
const collectionIds = await user.collectionIds();
const collectionScope = { method: ["withCollection", user.id] };
+ const viewScope = { method: ["withViews", user.id] };
const documents = await Document.scope(
"defaultScope",
- collectionScope
+ collectionScope,
+ viewScope
).findAll({
where: {
teamId: user.teamId,
@@ -214,7 +220,8 @@ router.post("documents.deleted", auth(), pagination(), async (ctx) => {
const collectionIds = await user.collectionIds({ paranoid: false });
const collectionScope = { method: ["withCollection", user.id] };
- const documents = await Document.scope(collectionScope).findAll({
+ const viewScope = { method: ["withViews", user.id] };
+ const documents = await Document.scope(collectionScope, viewScope).findAll({
where: {
teamId: user.teamId,
collectionId: collectionIds,
@@ -349,9 +356,11 @@ router.post("documents.drafts", auth(), pagination(), async (ctx) => {
const collectionIds = await user.collectionIds();
const collectionScope = { method: ["withCollection", user.id] };
+ const viewScope = { method: ["withViews", user.id] };
const documents = await Document.scope(
"defaultScope",
- collectionScope
+ collectionScope,
+ viewScope
).findAll({
where: {
userId: user.id,
diff --git a/server/models/Document.js b/server/models/Document.js
index e6f222e20..472fcdd97 100644
--- a/server/models/Document.js
+++ b/server/models/Document.js
@@ -207,11 +207,15 @@ Document.associate = (models) => {
{ model: models.User, as: "updatedBy", paranoid: false },
],
});
- Document.addScope("withViews", (userId) => ({
- include: [
- { model: models.View, as: "views", where: { userId }, required: false },
- ],
- }));
+ Document.addScope("withViews", (userId) => {
+ if (!userId) return {};
+
+ return {
+ include: [
+ { model: models.View, as: "views", where: { userId }, required: false },
+ ],
+ };
+ });
Document.addScope("withStarred", (userId) => ({
include: [
{ model: models.Star, as: "starred", where: { userId }, required: false },
@@ -222,9 +226,15 @@ Document.associate = (models) => {
Document.findByPk = async function (id, options = {}) {
// allow default preloading of collection membership if `userId` is passed in find options
// almost every endpoint needs the collection membership to determine policy permissions.
- const scope = this.scope("withUnpublished", {
- method: ["withCollection", options.userId],
- });
+ const scope = this.scope(
+ "withUnpublished",
+ {
+ method: ["withCollection", options.userId],
+ },
+ {
+ method: ["withViews", options.userId],
+ }
+ );
if (isUUID(id)) {
return scope.findOne({
diff --git a/server/models/View.js b/server/models/View.js
index b0d88e485..7607aebc8 100644
--- a/server/models/View.js
+++ b/server/models/View.js
@@ -2,7 +2,7 @@
import subMilliseconds from "date-fns/sub_milliseconds";
import { USER_PRESENCE_INTERVAL } from "../../shared/constants";
import { User } from "../models";
-import { Op, DataTypes, sequelize } from "../sequelize";
+import { DataTypes, Op, sequelize } from "../sequelize";
const View = sequelize.define(
"view",
diff --git a/server/presenters/document.js b/server/presenters/document.js
index 969555abe..c459db85f 100644
--- a/server/presenters/document.js
+++ b/server/presenters/document.js
@@ -1,6 +1,6 @@
// @flow
import { takeRight } from "lodash";
-import { User, Document, Attachment } from "../models";
+import { Attachment, Document, User } from "../models";
import { getSignedImageUrl } from "../utils/s3";
import presentUser from "./user";
@@ -62,8 +62,13 @@ export default async function present(document: Document, options: ?Options) {
pinned: undefined,
collectionId: undefined,
parentDocumentId: undefined,
+ lastViewedAt: undefined,
};
+ if (!!document.views && document.views.length > 0) {
+ data.lastViewedAt = document.views[0].updatedAt;
+ }
+
if (!options.isPublic) {
data.pinned = !!document.pinnedById;
data.collectionId = document.collectionId;