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;
- +