;
+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);
+ }
+}
diff --git a/src/components/PocketItem.tsx b/src/components/PocketItem.tsx
new file mode 100644
index 0000000..8d900f8
--- /dev/null
+++ b/src/components/PocketItem.tsx
@@ -0,0 +1,46 @@
+import { stylesheet } from "astroturf";
+import React from "react";
+import { SavedPocketItem } from "../PocketAPITypes";
+
+const styles = stylesheet`
+ .item {
+ color: black;
+ border: 1px solid black;
+ display: block;
+
+ padding: 4px 8px;
+ }
+ .item > span {
+ display: block;
+ }
+
+ .itemTitle {
+ font-weight: 600;
+ flex-grow: 1;
+ width: 100%;
+ }
+
+ .itemExcerpt {
+ font-weight: 300;
+ line-height: 1.5;
+ flex-grow: 1;
+ width: 100%;
+ }
+`;
+
+export type PocketItemProps = {
+ item: SavedPocketItem;
+};
+
+export const PocketItem = ({ item }: PocketItemProps) => {
+ const displayText =
+ item.resolved_title.length !== 0 ? item.resolved_title : item.resolved_url;
+ return (
+
+ {displayText}
+ {item.excerpt && (
+ {item.excerpt}
+ )}
+
+ );
+};
diff --git a/src/components/PocketItemList.tsx b/src/components/PocketItemList.tsx
new file mode 100644
index 0000000..83d1927
--- /dev/null
+++ b/src/components/PocketItemList.tsx
@@ -0,0 +1,60 @@
+import { stylesheet } from "astroturf";
+import React, { useEffect, useState } from "react";
+import { SavedPocketItem } from "src/PocketAPITypes";
+import { PocketItemStore } from "src/PocketItemStore";
+import { PocketItem } from "./PocketItem";
+
+const styles = stylesheet`
+ .list {
+ list-style-type: none;
+ }
+
+ .item {
+ margin: 8px;
+ }
+`;
+
+export type PocketItemListProps = {
+ itemStore: PocketItemStore;
+};
+
+export const PocketItemList = ({ itemStore }: PocketItemListProps) => {
+ const [items, setItems] = useState([]);
+
+ useEffect(() => {
+ var subscribed = true;
+ const fetch = async () => {
+ const allItems = await itemStore.getAllItemsBySortId();
+ subscribed && setItems(allItems);
+ };
+ fetch();
+
+ return () => {
+ subscribed = false;
+ };
+ }, []);
+
+ useEffect(() => {
+ const cbId = itemStore.subscribeOnChange(async () => {
+ const updatedItems = await itemStore.getAllItemsBySortId();
+ setItems(updatedItems);
+ });
+ return () => {
+ itemStore.unsubscribeOnChange(cbId);
+ };
+ }, [itemStore]);
+
+ if (items.length === 0) {
+ return <>No items synced!>;
+ } else {
+ return (
+
+ {items.map((item) => (
+ -
+
+
+ ))}
+
+ );
+ }
+};
diff --git a/src/main.ts b/src/main.ts
index 1f016cb..716da97 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,16 @@
import * as cors_proxy from "cors-anywhere";
import { App, Modal, Notice, Plugin } from "obsidian";
-import { openPocketItemStore, PocketItemStore } from "./pocket_item_store";
-import { PocketSettingTab } from "./settings";
+import ReactDOM from "react-dom";
+import { Username as PocketUsername } from "./PocketAPI";
+import { loadPocketAccessInfo } from "./PocketAuth";
+import {
+ PocketItemListView,
+ POCKET_ITEM_LIST_VIEW_TYPE,
+} from "./PocketItemListView";
+import { openPocketItemStore, PocketItemStore } from "./PocketItemStore";
+import { createReactApp } from "./ReactApp";
+import { PocketSettingTab } from "./Settings";
+import { ViewManager } from "./ViewManager";
interface PocketSyncSettings {
mySetting: string;
@@ -25,14 +34,20 @@ const setupCORSProxy = () => {
export default class PocketSync extends Plugin {
settings: PocketSyncSettings;
itemStore: PocketItemStore;
+ appEl: HTMLDivElement;
+ viewManager: ViewManager;
+ pocketUsername: PocketUsername | null;
+ pocketAuthenticated: boolean;
async onload() {
console.log("loading plugin");
await this.loadSettings();
+ // Set up CORS proxy for Pocket API calls
console.log("setting up CORS proxy");
setupCORSProxy();
+ // Set up Pocket item store
console.log("opening Pocket item store");
this.itemStore = await openPocketItemStore();
@@ -40,25 +55,9 @@ export default class PocketSync extends Plugin {
new Notice("This is a notice!");
});
- this.addStatusBarItem().setText("Status Bar Text");
+ this.addCommands();
- this.addCommand({
- id: "open-sample-modal",
- name: "Open Sample Modal",
- // callback: () => {
- // console.log('Simple Callback');
- // },
- checkCallback: (checking: boolean) => {
- let leaf = this.app.workspace.activeLeaf;
- if (leaf) {
- if (!checking) {
- new SampleModal(this.app).open();
- }
- return true;
- }
- return false;
- },
- });
+ this.addStatusBarItem().setText("Status Bar Text");
this.addSettingTab(new PocketSettingTab(this.app, this));
@@ -73,10 +72,44 @@ export default class PocketSync extends Plugin {
this.registerInterval(
window.setInterval(() => console.log("setInterval"), 5 * 60 * 1000)
);
+
+ const accessInfo = await loadPocketAccessInfo(this);
+ if (!accessInfo) {
+ console.log(`Not authenticated to Pocket`);
+ }
+
+ this.pocketAuthenticated = !!accessInfo;
+ this.pocketUsername = accessInfo?.username;
+
+ // Set up React-based Pocket item list view
+ this.viewManager = new ViewManager();
+ this.mount();
+ this.registerView(
+ POCKET_ITEM_LIST_VIEW_TYPE,
+ (leaf) => new PocketItemListView(leaf, this)
+ );
}
+ // Mount React app
+ mount = () => {
+ console.log("mounting React components");
+ ReactDOM.render(
+ createReactApp(this.viewManager),
+ this.appEl ?? (this.appEl = document.body.createDiv())
+ );
+ console.log("done mounting React components");
+ };
+
onunload() {
console.log("unloading plugin");
+
+ this.viewManager.clearViews();
+ this.viewManager = null;
+
+ if (this.appEl) {
+ ReactDOM.unmountComponentAtNode(this.appEl);
+ this.appEl.detach();
+ }
}
async loadSettings() {
@@ -86,6 +119,22 @@ export default class PocketSync extends Plugin {
async saveSettings() {
await this.saveData(this.settings);
}
+
+ openPocketList = async () => {
+ await this.app.workspace.activeLeaf.setViewState({
+ type: POCKET_ITEM_LIST_VIEW_TYPE,
+ });
+ };
+
+ addCommands = () => {
+ this.addCommand({
+ id: "open-pocket-list",
+ name: "Open Pocket list",
+ callback: () => {
+ this.openPocketList();
+ },
+ });
+ };
}
class SampleModal extends Modal {
diff --git a/src/pocket_item_store.ts b/src/pocket_item_store.ts
deleted file mode 100644
index 30dfbdd..0000000
--- a/src/pocket_item_store.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { IDBPDatabase, openDB } from "idb";
-import { UpdateTimestamp } from "./pocket_api";
-import {
- isDeletedPocketItem,
- isSavedPocketItem,
- PocketItemId,
- PocketItemRecord,
- SavedPocketItem,
-} from "./pocket_api_types";
-
-const DATABASE_NAME = "pocket_db";
-const ITEM_STORE_NAME = "items";
-
-const METADATA_STORE_NAME = "metadata";
-const LAST_UPDATED_TIMESTAMP_KEY = "last_updated_timestamp";
-
-export class PocketItemStore {
- db: IDBPDatabase;
-
- constructor(db: IDBPDatabase) {
- this.db = db;
- }
-
- mergeUpdates = async (
- lastUpdateTimestamp: UpdateTimestamp,
- items: PocketItemRecord
- ): Promise => {
- const updates = [];
-
- for (const key in items) {
- const item = items[key];
- if (isDeletedPocketItem(item)) {
- updates.push(this.deleteItem(item.item_id));
- } else if (isSavedPocketItem(item)) {
- updates.push(this.putItem(item));
- } else {
- throw new Error("unexpected");
- }
- }
-
- this.setLastUpdateTimestamp(lastUpdateTimestamp);
-
- // get Unix timestamp and write it
- await Promise.all(updates);
- };
-
- addItem = async (item: SavedPocketItem): Promise => {
- this.db.add(ITEM_STORE_NAME, item);
- };
-
- putItem = async (item: SavedPocketItem): Promise => {
- this.db.put(ITEM_STORE_NAME, item);
- };
-
- getItem = async (itemId: PocketItemId): Promise => {
- return this.db.get(ITEM_STORE_NAME, itemId);
- };
-
- deleteItem = async (itemId: PocketItemId): Promise => {
- this.db.delete(ITEM_STORE_NAME, itemId);
- };
-
- // 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
- ): Promise => {
- this.db.put(METADATA_STORE_NAME, timestamp, LAST_UPDATED_TIMESTAMP_KEY);
- };
-
- getLastUpdateTimestamp = async (): Promise => {
- return this.db.get(METADATA_STORE_NAME, LAST_UPDATED_TIMESTAMP_KEY);
- };
-}
-
-export const openPocketItemStore = async (): Promise => {
- const db = await openDB(DATABASE_NAME, 1, {
- upgrade: (db) => {
- const _itemStore = db.createObjectStore(ITEM_STORE_NAME, {
- keyPath: "item_id",
- });
-
- const _metadataStore = db.createObjectStore(METADATA_STORE_NAME);
- },
- });
- return new PocketItemStore(db);
-};
diff --git a/src/settings.ts b/src/settings.ts
index 4d658f8..c09f247 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -4,9 +4,9 @@ import {
OBSIDIAN_AUTH_PROTOCOL_ACTION,
setupAuth,
storePocketAccessInfo,
-} from "./auth";
+} from "./PocketAuth";
import PocketSync from "./main";
-import { getAccessToken, getPocketItems } from "./pocket_api";
+import { getAccessToken, getPocketItems } from "./PocketAPI";
const CONNECT_POCKET_CTA = "Connect your Pocket account";
const SYNC_POCKET_CTA = "Sync Pocket items";
@@ -45,7 +45,13 @@ const addTestAuthSetting = (plugin: PocketSync, containerEl: HTMLElement) =>
lastUpdateTimestamp
);
- plugin.itemStore.mergeUpdates(
+ console.log(
+ `Fetched ${
+ Object.keys(getPocketItemsResponse.response.list).length
+ } updates`
+ );
+
+ await plugin.itemStore.mergeUpdates(
getPocketItemsResponse.timestamp,
getPocketItemsResponse.response.list
);
@@ -66,6 +72,8 @@ export class PocketSettingTab extends PluginSettingTab {
async (params) => {
const accessInfo = await getAccessToken();
storePocketAccessInfo(this.plugin, accessInfo);
+ this.plugin.pocketAuthenticated = true;
+ this.plugin.pocketUsername = accessInfo.username;
}
);
diff --git a/styles.css b/styles.css
index e69de29..fb4e2cb 100644
--- a/styles.css
+++ b/styles.css
@@ -0,0 +1,30 @@
+.PocketItem-styles-module_item__2EqeQ {
+ color: black;
+ border: 1px solid black;
+ display: block;
+
+ padding: 4px 8px;
+ }
+ .PocketItem-styles-module_item__2EqeQ > span {
+ display: block;
+ }
+
+ .PocketItem-styles-module_itemTitle__1Y0M_ {
+ font-weight: 600;
+ flex-grow: 1;
+ width: 100%;
+ }
+
+ .PocketItem-styles-module_itemExcerpt__21asC {
+ font-weight: 300;
+ line-height: 1.5;
+ flex-grow: 1;
+ width: 100%;
+ }
+.PocketItemList-styles-module_list__2MnxN {
+ list-style-type: none;
+ }
+
+ .PocketItemList-styles-module_item__1WnQ- {
+ margin: 8px;
+ }
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index 558e259..1df4878 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,7 +10,8 @@
"moduleResolution": "node",
"importHelpers": true,
"lib": ["dom", "es5", "scripthost", "es2015"],
- "esModuleInterop": true
+ "esModuleInterop": true,
+ "jsx": "react"
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]