diff --git a/.env.example b/.env.example
index 6050859..a3f88a1 100644
--- a/.env.example
+++ b/.env.example
@@ -9,4 +9,7 @@ JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA/nqSs2DZmox+sRNR9d9XdaO3C2yJABIO5gdJlBcswNI=
-----END PUBLIC KEY-----"
+PUBLIC_PUSHER_APP_KEY="3a4575271634ad5a09ef"
+PUBLIC_PUSHER_APP_CLUSTER="eu"
+
PUBLIC_LIVEBLOCKS_API_KEY="pk_dev_Nwp6X3J4wQWoOKenVZfTTo20uVznlY5ir2_W95bx0RjleqvW7LKKMlPbCZISFsYI"
diff --git a/package-lock.json b/package-lock.json
index f06c52d..3371e39 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,6 +33,7 @@
"flowbite-typography": "^1.0.3",
"jose": "^4.15.1",
"nanostores": "^0.9.3",
+ "pusher-js": "^8.3.0",
"tailwind-scrollbar": "^3.0.5",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
@@ -7782,6 +7783,14 @@
"node": ">=6"
}
},
+ "node_modules/pusher-js": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.3.0.tgz",
+ "integrity": "sha512-6GohP06WlVeomAQQe9qWh1IDzd3+InluWt+ZUOcecVK1SEQkg6a8uYVsvxSJm7cbccfmHhE0jDkmhKIhue8vmA==",
+ "dependencies": {
+ "tweetnacl": "^1.0.3"
+ }
+ },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@@ -9113,6 +9122,11 @@
"node": "*"
}
},
+ "node_modules/tweetnacl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
+ },
"node_modules/type-detect": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
diff --git a/package.json b/package.json
index 23957ea..127de6c 100644
--- a/package.json
+++ b/package.json
@@ -52,6 +52,7 @@
"flowbite-typography": "^1.0.3",
"jose": "^4.15.1",
"nanostores": "^0.9.3",
+ "pusher-js": "^8.3.0",
"tailwind-scrollbar": "^3.0.5",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
diff --git a/src/components/TestForm.vue b/src/components/TestForm.vue
deleted file mode 100644
index 8315111..0000000
--- a/src/components/TestForm.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
diff --git a/src/components/item/Browser.vue b/src/components/item/Browser.vue
index 8a59db1..5abd0e8 100644
--- a/src/components/item/Browser.vue
+++ b/src/components/item/Browser.vue
@@ -78,7 +78,7 @@ import { fetchFromApi } from '@lib/helpers';
// Stores
import { useStore } from '@nanostores/vue';
-import { addItem, itemsStore } from '@stores/items';
+import { addItem, removeItem, itemsStore } from '@stores/items';
import { isModalOpen } from '@stores/modal';
// Components
@@ -90,6 +90,10 @@ const props = defineProps({
type: Object as PropType,
required: false,
},
+ user: {
+ type: Object as PropType,
+ required: true,
+ },
});
const fileBrowserContextMenu = ref>();
@@ -109,7 +113,7 @@ const toasts = ref<{ message: string; type: ToastType }[]>([]);
/**
* Items
*/
-import { ItemClass } from '@lib/items/items';
+import { ItemClass, type ItemType } from '@lib/items/items';
import { ItemFactory } from '@lib/items/factory';
import NoFiles from './file/NoFiles.vue';
@@ -192,16 +196,6 @@ async function uploadFiles(e: Event) {
Array.from(fileInput.files).forEach(async (file) => {
try {
await FileClass.create(file, props.modelValue ?? null);
-
- // TODO: replace waiting 1 second with websocket
- setTimeout(async () => {
- await getItems();
-
- toasts.value.push({
- message: `File ${file.name} uploaded`,
- type: ToastType.Success,
- });
- }, 1000);
} catch (error) {
toasts.value.push({
message: `Failed to upload file ${file.name}`,
@@ -223,4 +217,41 @@ const createDocsModal = ref>();
const docs = computed(() => {
return Object.values(items.value).filter((item) => item instanceof DocsClass) as DocsClass[];
});
+
+/**
+ * Live Updates
+ */
+import { getFolderChannel } from '@lib/pusher';
+
+const channel = getFolderChannel(props.user.id, props.modelValue?.id);
+channel.bind('update', (data: ItemType) => {
+ if (!ItemClass.isItem(data)) return;
+
+ const item = ItemFactory.getItemFromObject(data);
+
+ if (item === null) return;
+
+ const isNew = items.value[item.id] === undefined;
+ const isOwner = item.ownerId === props.user.id;
+
+ if (isNew && isOwner) {
+ // TODO: Fix toasts
+ toasts.value.push({
+ message: `${item.name} has been created`,
+ type: ToastType.Success,
+ });
+ }
+
+ addItem(item);
+});
+
+channel.bind('delete', (data: ItemType) => {
+ if (!ItemClass.isItem(data)) return;
+
+ const item = ItemFactory.getItemFromObject(data);
+
+ if (item === null) return;
+
+ removeItem(item);
+});
diff --git a/src/components/item/ShareItemModal.vue b/src/components/item/ShareItemModal.vue
index ab9b27a..d649624 100644
--- a/src/components/item/ShareItemModal.vue
+++ b/src/components/item/ShareItemModal.vue
@@ -154,6 +154,10 @@ async function getItem() {
rawItem.ItemSharing.forEach(
(sharing: { id: number; user: { id: number; name: string; email: string } }) => {
+ if (sharing.user.id === rawItem.owner.id) {
+ return;
+ }
+
usersWithAccess.value.push({
id: sharing.user.id,
sharingId: sharing.id,
diff --git a/src/components/item/docs/EditModal.vue b/src/components/item/docs/EditModal.vue
index 50f0825..01cf80c 100644
--- a/src/components/item/docs/EditModal.vue
+++ b/src/components/item/docs/EditModal.vue
@@ -64,10 +64,15 @@ async function updateFile() {
// TODO: Show success toast
close(false);
- } catch (e) {}
+ } catch (e) {
+ console.error('Error: ' + e);
+ }
}
function open() {
+ docs.value = {
+ name: props.docs.name,
+ };
modal.value?.open();
}
diff --git a/src/components/item/file/EditModal.vue b/src/components/item/file/EditModal.vue
index dfbe3b1..3464af7 100644
--- a/src/components/item/file/EditModal.vue
+++ b/src/components/item/file/EditModal.vue
@@ -70,6 +70,9 @@ async function updateFile() {
}
function open() {
+ file.value = {
+ name: props.file.name,
+ };
modal.value?.open();
}
diff --git a/src/components/item/folder/EditModal.vue b/src/components/item/folder/EditModal.vue
index 04a790f..567e8f9 100644
--- a/src/components/item/folder/EditModal.vue
+++ b/src/components/item/folder/EditModal.vue
@@ -108,6 +108,10 @@ async function updateFolder() {
}
function open() {
+ folder.value = {
+ name: props.folder.name,
+ color: props.folder.color,
+ };
modal.value?.open();
}
diff --git a/src/env.d.ts b/src/env.d.ts
index 9372e88..d84b5d6 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -1,6 +1,7 @@
///
interface User {
+ id: number;
name: string;
email: string;
}
@@ -13,6 +14,8 @@ interface ImportMetaEnv {
readonly PUBLIC_LOCAL_DEVELOPMENT_API_URL: string | undefined;
readonly JWT_PUBLIC_KEY: string;
readonly NODE_BUILD: boolean | undefined;
+ readonly PUBLIC_PUSHER_APP_KEY: string;
+ readonly PUBLIC_PUSHER_APP_CLUSTER: string;
}
interface ImportMeta {
diff --git a/src/lang/en.ts b/src/lang/en.ts
index e54e1b4..593d002 100644
--- a/src/lang/en.ts
+++ b/src/lang/en.ts
@@ -51,7 +51,7 @@ export default {
submit: 'Edit',
},
action: {
- create: 'Upload fil',
+ create: 'Upload file',
edit: 'Rename file',
share: 'Share',
delete: 'Delete',
@@ -112,12 +112,12 @@ export default {
submit: 'Edit',
},
action: {
- create: 'Create Docs',
- edit: 'Rename Docs',
+ create: 'Create docs',
+ edit: 'Rename docs',
share: 'Share',
delete: 'Delete',
openInNewTab: 'Open in new tab',
- confirmDelete: 'Are you sure you want to delete this Docs?',
+ confirmDelete: 'Are you sure you want to delete this docs?',
},
},
},
diff --git a/src/lib/items/docs.ts b/src/lib/items/docs.ts
index 6224a29..3bce554 100644
--- a/src/lib/items/docs.ts
+++ b/src/lib/items/docs.ts
@@ -1,6 +1,6 @@
import { ItemClass, type ItemType } from './items';
import { type FolderType } from './folders';
-import { api } from '@lib/helpers';
+import { api, fetchFromApi } from '@lib/helpers';
export class DocsClass extends ItemClass {
private _text: string;
diff --git a/src/lib/pusher.ts b/src/lib/pusher.ts
new file mode 100644
index 0000000..c22bd93
--- /dev/null
+++ b/src/lib/pusher.ts
@@ -0,0 +1,15 @@
+import Pusher from 'pusher-js';
+
+Pusher.logToConsole = true;
+
+const pusher = new Pusher(import.meta.env.PUBLIC_PUSHER_APP_KEY, {
+ cluster: import.meta.env.PUBLIC_PUSHER_APP_CLUSTER,
+});
+
+const getFolderChannel = (userId: number, itemId: number | null | undefined) => {
+ const channelName = itemId ? `browser-folder-${itemId}` : `browser-root-${userId}`;
+
+ return pusher.subscribe(channelName);
+};
+
+export { pusher, getFolderChannel };
diff --git a/src/middleware/user.ts b/src/middleware/user.ts
index 76cc85c..44f8e77 100644
--- a/src/middleware/user.ts
+++ b/src/middleware/user.ts
@@ -30,7 +30,7 @@ export const user = defineMiddleware(async ({ locals, request, redirect }, next)
}
// Otherwise, set the user data in the locals.
- locals.user = userResponse as User;
+ locals.user = { id: locals.userId, ...userResponse } as User;
return next();
});
diff --git a/src/pages/u/folder/[id].astro b/src/pages/u/folder/[id].astro
index 1b094a5..9111f23 100644
--- a/src/pages/u/folder/[id].astro
+++ b/src/pages/u/folder/[id].astro
@@ -38,6 +38,6 @@ const user = Astro.locals.user as User;
-
+
diff --git a/src/pages/u/index.astro b/src/pages/u/index.astro
index e3fa003..5602c76 100644
--- a/src/pages/u/index.astro
+++ b/src/pages/u/index.astro
@@ -9,6 +9,6 @@ const user = Astro.locals.user as User;
-
+