Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pocket item list is now rendered #2

Merged
merged 11 commits into from
Jun 18, 2021
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ data.json

# Pocket integration
.__pocket_access_info__

# Astroturf css
**/*.module.css
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,26 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^18.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.2.1",
"@rollup/plugin-typescript": "^8.2.1",
"@types/jest": "^26.0.23",
"@types/node": "^14.14.37",
"@types/qs": "^6.9.6",
"@types/react": "^17.0.11",
"@types/react-dom": "^17.0.7",
"@types/sqlite3": "^3.1.7",
"@types/uuid": "^8.3.0",
"astroturf": "^1.0.0-beta.21",
"jest": "^27.0.4",
"obsidian": "^0.12.0",
"postcss": "^8.3.5",
"postcss-nested": "^5.0.5",
"rollup": "^2.32.1",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-postcss": "^4.0.0",
"ts-jest": "^27.0.2",
"ts-node": "^10.0.0",
"tslib": "^2.2.0",
Expand All @@ -31,7 +40,11 @@
"dependencies": {
"cors-anywhere": "^0.4.4",
"idb": "^6.1.2",
"qs": "^6.10.1"
"immutability-helper": "^3.1.1",
"qs": "^6.10.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"uuid": "^8.3.2"
},
"jest": {
"clearMocks": true,
Expand Down
23 changes: 21 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import typescript from "@rollup/plugin-typescript";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
import babel from "@rollup/plugin-babel";
import postcss from "rollup-plugin-postcss";
import postcssNested from "postcss-nested";
import del from "rollup-plugin-delete";

const isProd = process.env.BUILD === "production";

Expand All @@ -23,11 +27,26 @@ export default {
},
external: ["obsidian"],
plugins: [
typescript(),
nodeResolve({ browser: true }),
commonjs({
include: "node_modules/**",
}),
babel({
babelHelpers: "bundled",
extensions: [".tsx", ".ts"],
plugins: ["astroturf/plugin"],
}),
postcss({
extract: "styles.css",
modules: true,
plugins: [postcssNested],
}),
typescript(),
nodeResolve({ browser: true }),
json(),
del({
targets: ["**/*.module.css"],
hook: "buildEnd",
runOnce: true,
}),
],
};
2 changes: 1 addition & 1 deletion src/pocket_api.ts → src/PocketAPI.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as qs from "qs";
import { PocketGetItemsResponse } from "./pocket_api_types";
import { PocketGetItemsResponse } from "./PocketAPITypes";

export type RequestToken = string;
export type AccessToken = string;
Expand Down
3 changes: 3 additions & 0 deletions src/pocket_api_types.ts → src/PocketAPITypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export interface DeletedPocketItem extends BasePocketItem {

export interface SavedPocketItem extends BasePocketItem {
status: PocketItemStatus.Unread | PocketItemStatus.Archived;
resolved_title: string;
resolved_url: string;
excerpt: string;
}

export type PocketItem = SavedPocketItem | DeletedPocketItem;
Expand Down
2 changes: 1 addition & 1 deletion src/auth.ts → src/PocketAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
buildAuthorizationURL,
getRequestToken,
RequestToken,
} from "./pocket_api";
} from "./PocketAPI";

export type AccessInfo = AccessTokenResponse;

Expand Down
41 changes: 41 additions & 0 deletions src/PocketItemListView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import React from "react";
import PocketSync from "./main";
import { PocketItemList } from "./components/PocketItemList";

export const POCKET_ITEM_LIST_VIEW_TYPE = "pocket_item_list";

export class PocketItemListView extends ItemView {
plugin: PocketSync;
id: string = (this.leaf as any).id;

constructor(leaf: WorkspaceLeaf, plugin: PocketSync) {
// TODO: Get the username in here
super(leaf);

if (!plugin.pocketAuthenticated) {
throw new Error(
"Tried to display PocketItemListView when not Pocket-authenticated"
);
}

this.plugin = plugin;
this.plugin.viewManager.addView(this.id, this);
}

getViewType(): string {
return POCKET_ITEM_LIST_VIEW_TYPE;
}
getDisplayText(): string {
return `Pocket list for ${this.plugin.pocketUsername}`;
}

async onClose() {
console.log("onClose");
this.plugin.viewManager.removeView(this.id);
}

getPortal() {
return <PocketItemList itemStore={this.plugin.itemStore} />;
}
}
154 changes: 154 additions & 0 deletions src/PocketItemStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { IDBPDatabase, openDB } from "idb";
import { v4 as uuidv4 } from "uuid";
import { UpdateTimestamp } from "./PocketAPI";
import {
isDeletedPocketItem,
isSavedPocketItem,
PocketItemId,
PocketItemRecord,
SavedPocketItem,
} from "./PocketAPITypes";
import { ViewName } from "./ViewManager";

const DATABASE_NAME = "pocket_db";
const ITEM_STORE_NAME = "items";

const METADATA_STORE_NAME = "metadata";
const LAST_UPDATED_TIMESTAMP_KEY = "last_updated_timestamp";

export type OnChangeCallback = () => Promise<void>;
export type CallbackId = string;

export class PocketItemStore {
db: IDBPDatabase;
onChangeCallbacks: Map<ViewName, OnChangeCallback>;

constructor(db: IDBPDatabase) {
this.db = db;
this.onChangeCallbacks = new Map();
}

mergeUpdates = async (
lastUpdateTimestamp: UpdateTimestamp,
items: PocketItemRecord
): Promise<void> => {
const updates = [];

// TODO: Should all of this be happening in a transaction?
console.log("Applying updates");
for (const key in items) {
const item = items[key];
if (isDeletedPocketItem(item)) {
updates.push(this.deleteItem(item.item_id, false));
} else if (isSavedPocketItem(item)) {
updates.push(this.putItem(item, false));
} else {
throw new Error("unexpected");
}
}

// Wait on all changes, update timestamp, then trigger registered onChange handlers
await Promise.all(updates);
console.log("Updates applied");
this.setLastUpdateTimestamp(lastUpdateTimestamp);
console.log("Running onChange handlers");
await this.handleOnChange();
};

addItem = async (item: SavedPocketItem, triggerOnChangeHandlers = true) => {
await this.db.add(ITEM_STORE_NAME, item);
triggerOnChangeHandlers && (await this.handleOnChange());
};

putItem = async (
item: SavedPocketItem,
triggerOnChangeHandlers?: boolean
) => {
await this.db.put(ITEM_STORE_NAME, item);
triggerOnChangeHandlers && (await this.handleOnChange());
};

getItem = async (itemId: PocketItemId): Promise<SavedPocketItem | null> => {
return this.db.get(ITEM_STORE_NAME, itemId);
};

getAllItems = async (): Promise<SavedPocketItem[]> => {
return this.db.getAll(ITEM_STORE_NAME);
};

getAllItemsBySortId = async (): Promise<SavedPocketItem[]> => {
return this.db.getAllFromIndex(ITEM_STORE_NAME, "sort_id");
};

deleteItem = async (
itemId: PocketItemId,
triggerOnChangeHandlers?: boolean
) => {
await this.db.delete(ITEM_STORE_NAME, itemId);
triggerOnChangeHandlers && (await this.handleOnChange());
};

// This Unix timestamp is the last time that the Pocket item store was synced
// via the Pocket API. It is used in subsequent requests to only get updates
// since the timestamp. If the timestamp is null, it means that no requests
// have been done so far.

setLastUpdateTimestamp = async (
timestamp: UpdateTimestamp,
triggerOnChangeHandlers?: boolean
): Promise<void> => {
console.log("Updating update timestamp");
await this.db.put(
METADATA_STORE_NAME,
timestamp,
LAST_UPDATED_TIMESTAMP_KEY
);
triggerOnChangeHandlers && (await this.handleOnChange());
};

getLastUpdateTimestamp = async (): Promise<UpdateTimestamp | null> => {
return this.db.get(METADATA_STORE_NAME, LAST_UPDATED_TIMESTAMP_KEY);
};

subscribeOnChange = (cb: OnChangeCallback): CallbackId => {
const callbackId = uuidv4();
this.onChangeCallbacks.set(callbackId, cb);
return callbackId;
};

unsubscribeOnChange = (cbId: CallbackId): void => {
this.onChangeCallbacks.delete(cbId);
};

private handleOnChange = async () => {
const cbExecs = Array.from(this.onChangeCallbacks.values()).map((cb) =>
cb()
);
await Promise.all(cbExecs);
};
}

export const openPocketItemStore = async (): Promise<PocketItemStore> => {
const dbVersion = 2;
const db = await openDB(DATABASE_NAME, dbVersion, {
upgrade: (db, oldVersion, newVersion, tx) => {
if (oldVersion !== newVersion) {
console.log(
`Upgrading pocket item store to version ${newVersion} from version ${oldVersion}`
);
}

switch (oldVersion) {
case 0:
db.createObjectStore(ITEM_STORE_NAME, {
keyPath: "item_id",
});
db.createObjectStore(METADATA_STORE_NAME);
case 1:
const itemStore = tx.objectStore(ITEM_STORE_NAME);
itemStore.createIndex("sort_id", "sort_id", { unique: false });
}
},
});
return new PocketItemStore(db);
};
36 changes: 36 additions & 0 deletions src/ReactApp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";
import { createPortal } from "react-dom";
import { PocketItemListView } from "./PocketItemListView";
import { ViewManager } from "./ViewManager";

export const createReactApp = (viewManager: ViewManager) => (
<ReactApp viewManager={viewManager} />
);

export type ViewProps = {
view: PocketItemListView;
};

export class View extends React.Component<ViewProps> {
constructor(props: ViewProps) {
super(props);
}

render() {
return createPortal(this.props.view.getPortal(), this.props.view.contentEl);
}
}

export type ReactAppProps = {
viewManager: ViewManager;
};

export const ReactApp = ({ viewManager }: ReactAppProps) => {
const [views, _setViews] = viewManager.useState();
const portals = [];
for (const view of Array.from(views.values())) {
portals.push(<View key={view.id} view={view} />);
}

return <>{...portals}</>;
};
44 changes: 44 additions & 0 deletions src/ViewManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import update from "immutability-helper";
import { useEffect, useState } from "react";
import { PocketItemListView } from "./PocketItemListView";

export type ViewName = string;
export type ViewMapping = Map<ViewName, PocketItemListView>;
export type SetViewMapping = (viewMapping: ViewMapping) => void;

export class ViewManager {
views: ViewMapping;
setState: SetViewMapping;

constructor() {
this.views = new Map();
}

useState(): [ViewMapping, SetViewMapping] {
const [state, setState] = useState(this.views);

// Make sure setState reference is stored in this instance
useEffect(() => {
this.setState = setState;
}, [this]);

return [state, setState];
}

addView(viewName: ViewName, view: PocketItemListView): void {
console.log(`Adding view for ${viewName}`);
this.views = update(this.views, { $add: [[viewName, view]] });
this.setState(this.views);
console.log(`views: ${Array.from(this.views.keys())}`);
}
removeView(viewName: ViewName): void {
console.log(`Removing view for ${viewName}`);
this.views = update(this.views, { $remove: [viewName] });
this.setState(this.views);
console.log(`views: ${Array.from(this.views.keys())}`);
}
clearViews(): void {
this.views = update(this.views, { $set: new Map() });
this.setState(this.views);
}
}
Loading