@dropby/vue 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/chunks/chat-attachment-types-BG8VFQj5.d.ts +56 -0
- package/dist/chunks/chat-attachments-BeJa4tEd.js +20 -0
- package/dist/chunks/public-component-A2rcQYAZ.d.ts +10 -0
- package/dist/chunks/use-drop-by-4B6MMMx1.js +33 -0
- package/dist/chunks/use-feature-BQHA7pdn.js +68 -0
- package/dist/components/chat-attachments/index.d.ts +9 -0
- package/dist/components/chat-attachments/index.js +87 -0
- package/dist/components/index.d.ts +402 -0
- package/dist/components/index.js +1125 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +33 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simpllyf Software
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# `@dropby/vue`
|
|
2
|
+
|
|
3
|
+
DropBy for Vue 3: a provider, composables for support chat, ideas, pending asks
|
|
4
|
+
and feature flags, and optional unstyled components. It supports Vue 3.4 and
|
|
5
|
+
later 3.x releases.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @dropby/vue @dropby/browser
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@dropby/browser` holds the options and data types, and its README explains
|
|
14
|
+
sessions, context and flags in full. Keep the two packages on the same version;
|
|
15
|
+
every release publishes them together.
|
|
16
|
+
|
|
17
|
+
## Provider and composables
|
|
18
|
+
|
|
19
|
+
With `options`, `DropByProvider` creates the DropBy instance, starts it once
|
|
20
|
+
mounted, and destroys it with its scope. Your `auth` callback calls your own
|
|
21
|
+
backend, which knows who is signed in and creates the session; see
|
|
22
|
+
[`@dropby/server`](https://www.npmjs.com/package/@dropby/server).
|
|
23
|
+
|
|
24
|
+
```vue
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { onMounted, ref } from "vue";
|
|
27
|
+
import type { DropByOptions } from "@dropby/browser";
|
|
28
|
+
import { DropByProvider } from "@dropby/vue";
|
|
29
|
+
import Inbox from "./Inbox.vue";
|
|
30
|
+
|
|
31
|
+
const options: DropByOptions = {
|
|
32
|
+
publicKey: "YOUR_PUBLIC_KEY",
|
|
33
|
+
auth: async ({ refresh }) => {
|
|
34
|
+
const response = await fetch("/api/dropby/session", {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "content-type": "application/json" },
|
|
37
|
+
body: JSON.stringify({ refresh }),
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) throw new Error("could not create a DropBy session");
|
|
40
|
+
return response.json();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const mounted = ref(false);
|
|
45
|
+
onMounted(() => {
|
|
46
|
+
mounted.value = true;
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<DropByProvider v-if="mounted" :options="options">
|
|
52
|
+
<Inbox />
|
|
53
|
+
</DropByProvider>
|
|
54
|
+
</template>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`Inbox.vue` lists the unread conversations. `useChatThread(id)` and
|
|
58
|
+
`useFeatureFlag(name)` also take a ref or a getter, and follow it when it
|
|
59
|
+
changes.
|
|
60
|
+
|
|
61
|
+
<!-- snippet file=Inbox.vue -->
|
|
62
|
+
|
|
63
|
+
```vue
|
|
64
|
+
<script setup lang="ts">
|
|
65
|
+
import { onMounted } from "vue";
|
|
66
|
+
import { useChatThreads } from "@dropby/vue";
|
|
67
|
+
|
|
68
|
+
const { data, actions } = useChatThreads();
|
|
69
|
+
onMounted(() => void actions.listThreads({ filter: "unread" }));
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<p>{{ data.items.length }} unread threads</p>
|
|
74
|
+
</template>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `useChatThreads`, `useChatThread(id)`, `useChatUnread`, `useIdeas` and
|
|
78
|
+
`usePendingAsks` return `data` as a read-only ref and a stable `actions`
|
|
79
|
+
object.
|
|
80
|
+
- `useDropBy()` returns the instance. `useDropByState()` returns its live
|
|
81
|
+
`bootstrap`, `capabilities` and `connection` refs.
|
|
82
|
+
- An instance passed as the provider's `dropby` prop stays yours to start and
|
|
83
|
+
destroy.
|
|
84
|
+
- The `v-if` above mounts DropBy only in the browser. Never share an instance
|
|
85
|
+
across server requests.
|
|
86
|
+
- The provider reads `options` once. When a user logs out, or another user or
|
|
87
|
+
account signs in, mount a new provider (give it a `:key` of the user's and
|
|
88
|
+
account's ids) so the old instance is destroyed.
|
|
89
|
+
- Use the full `pk_live_...` or `pk_test_...` value from the dashboard's API
|
|
90
|
+
keys page for `YOUR_PUBLIC_KEY`.
|
|
91
|
+
|
|
92
|
+
## Feature flags
|
|
93
|
+
|
|
94
|
+
`useFeatureFlag(name)` returns one flag's value as a ref, and `useFeatureConfig()`
|
|
95
|
+
all of them. `<Feature>` renders its default slot when a flag is on (truthy, or
|
|
96
|
+
equal to `equals`), and its `fallback` slot otherwise. Before the instance
|
|
97
|
+
starts, and for a flag that does not exist, the value is `undefined`. How flags
|
|
98
|
+
are resolved, and when they change, is in the
|
|
99
|
+
[`@dropby/browser` README](https://www.npmjs.com/package/@dropby/browser#context-and-feature-flags).
|
|
100
|
+
|
|
101
|
+
```vue
|
|
102
|
+
<script setup lang="ts">
|
|
103
|
+
import { useFeatureFlag } from "@dropby/vue";
|
|
104
|
+
import { Feature } from "@dropby/vue/components";
|
|
105
|
+
|
|
106
|
+
const format = useFeatureFlag("export_format");
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<Feature name="new_exports">
|
|
111
|
+
<button type="button">export as {{ format === "csv" ? "csv" : "pdf" }}</button>
|
|
112
|
+
<template #fallback><span>exports are coming soon</span></template>
|
|
113
|
+
</Feature>
|
|
114
|
+
</template>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Components and styling
|
|
118
|
+
|
|
119
|
+
Optional components live in `@dropby/vue/components`: `ChatThread`,
|
|
120
|
+
`ChatThreadList`, `ChatComposer`, `IdeaList`, `Idea`, `IdeaVoteButton`, `Ask`,
|
|
121
|
+
`AskForm`, `PendingAsks` and `Feature`. They render plain, accessible HTML with
|
|
122
|
+
state attributes such as `data-status`, `data-unread` and `data-busy`, so your
|
|
123
|
+
own CSS styles them; they load no theme. Each, except `Feature`, also takes a
|
|
124
|
+
default scoped slot when you want your own markup with its state, and the slot
|
|
125
|
+
props are typed:
|
|
126
|
+
|
|
127
|
+
```vue
|
|
128
|
+
<script setup lang="ts">
|
|
129
|
+
import { ChatThreadList } from "@dropby/vue/components";
|
|
130
|
+
|
|
131
|
+
const emit = defineEmits<{ open: [threadId: `thr_${string}`] }>();
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<template>
|
|
135
|
+
<ChatThreadList v-slot="{ threads }">
|
|
136
|
+
<button
|
|
137
|
+
v-for="thread in threads.items"
|
|
138
|
+
:key="thread.threadId"
|
|
139
|
+
type="button"
|
|
140
|
+
@click="emit('open', thread.threadId)"
|
|
141
|
+
>
|
|
142
|
+
{{ thread.lastMessagePreview }}
|
|
143
|
+
</button>
|
|
144
|
+
<p v-if="threads.status === 'error'">couldn't load your conversations.</p>
|
|
145
|
+
</ChatThreadList>
|
|
146
|
+
</template>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The default chat components behave like the hosted widget:
|
|
150
|
+
|
|
151
|
+
- `ChatThread` marks a reply seen once its end is on screen (the page visible,
|
|
152
|
+
the viewer allowed). Pass `:acknowledge="false"` to do that yourself with
|
|
153
|
+
`actions.markSeen`. A slot body is never observed.
|
|
154
|
+
- `ChatThread` follows new messages inside its own scroll container: to the
|
|
155
|
+
newest when it opens, when the reader sends, and when a message arrives while
|
|
156
|
+
they are at the end. It scrolls only its own scroll container (itself or the
|
|
157
|
+
nearest ancestor with `overflow-y: auto`), never the document.
|
|
158
|
+
- `ChatThreadList` rows become buttons with `@select="(threadId) => ..."`;
|
|
159
|
+
`selected-id` marks the open one with `aria-current`.
|
|
160
|
+
- Failed loads, sends and downloads show as `role="alert"` messages in plain
|
|
161
|
+
words. A slot's state carries the `DropByError` behind a failed load or send.
|
|
162
|
+
- `ChatComposer` keeps a failed message in the box. Sending it again unchanged
|
|
163
|
+
cannot post it twice.
|
|
164
|
+
|
|
165
|
+
## Optional attachments
|
|
166
|
+
|
|
167
|
+
Import `useChatAttachments` only when the composer needs uploads, so other
|
|
168
|
+
pages never load the upload code:
|
|
169
|
+
|
|
170
|
+
```vue
|
|
171
|
+
<script setup lang="ts">
|
|
172
|
+
import { ChatComposer } from "@dropby/vue/components";
|
|
173
|
+
import { useChatAttachments } from "@dropby/vue/components/chat-attachments";
|
|
174
|
+
|
|
175
|
+
defineProps<{ threadId: `thr_${string}` }>();
|
|
176
|
+
const attachments = useChatAttachments();
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<template>
|
|
180
|
+
<ChatComposer :thread-id="threadId" :attachments="attachments" />
|
|
181
|
+
</template>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The composable returns `drafts`, `add`, `remove`, `clear`, `references`,
|
|
185
|
+
`pending`, `notice` and `accept`. `ChatComposer` waits for uploads in progress
|
|
186
|
+
and sends only the ready ones. Disposing the component's scope releases the
|
|
187
|
+
composable's previews and subscriptions; destroying the instance cancels its
|
|
188
|
+
uploads.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ChatAttachmentContentType, ChatAttachmentKind, DropById } from "@dropby/browser";
|
|
2
|
+
/** A file picked for the next message: its upload's state, and a preview for images. */
|
|
3
|
+
interface ChatAttachmentDraft {
|
|
4
|
+
/** This draft's own id, stable from the moment it is picked; pass it to `remove`. */
|
|
5
|
+
localId: string;
|
|
6
|
+
/** The file's name, trimmed, or `attachment` when it has none. */
|
|
7
|
+
fileName: string;
|
|
8
|
+
/** The type the file was accepted as; null until it is checked. */
|
|
9
|
+
contentType: ChatAttachmentContentType | null;
|
|
10
|
+
/** The file's size in bytes. */
|
|
11
|
+
byteSize: number;
|
|
12
|
+
/** Whether it shows as an image or a file; null until it is checked. */
|
|
13
|
+
kind: ChatAttachmentKind | null;
|
|
14
|
+
/** A local URL to preview an image; null for other files. */
|
|
15
|
+
previewUrl: string | null;
|
|
16
|
+
/** Where the upload is: checking the file, uploading, ready to send, or failed. */
|
|
17
|
+
status: "checking" | "uploading" | "ready" | "error";
|
|
18
|
+
/** The attachment's id once it is ready to send; null before. */
|
|
19
|
+
attachmentId: DropById<"att_"> | null;
|
|
20
|
+
/** Why the upload failed, such as `too_large`; null unless `status` is `error`. */
|
|
21
|
+
error: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
} | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Attachments for a composer, from `useChatAttachments`: the files picked, and
|
|
28
|
+
* how to add, remove and send them. Pass it to `ChatComposer` as `attachments`.
|
|
29
|
+
*/
|
|
30
|
+
interface ChatAttachmentsToolkit {
|
|
31
|
+
/** The files picked, in order. */
|
|
32
|
+
drafts: ChatAttachmentDraft[];
|
|
33
|
+
/**
|
|
34
|
+
* Starts uploading files, up to four per message; files past that are left out
|
|
35
|
+
* and `notice` says so.
|
|
36
|
+
*/
|
|
37
|
+
add: (files: FileList | File[]) => void;
|
|
38
|
+
/** Drops one file from the message. */
|
|
39
|
+
remove: (localId: string) => void;
|
|
40
|
+
/** Drops every file. */
|
|
41
|
+
clear: () => void;
|
|
42
|
+
/** The ready uploads, to send with a message. */
|
|
43
|
+
references: () => Array<{
|
|
44
|
+
attachmentId: DropById<"att_">;
|
|
45
|
+
}>;
|
|
46
|
+
/** Whether a file is still being checked or uploaded. */
|
|
47
|
+
pending: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Why files were left out of the last `add`; null otherwise. Cleared by the next
|
|
50
|
+
* `add` that takes every file, or by any `remove` or `clear`.
|
|
51
|
+
*/
|
|
52
|
+
notice: string | null;
|
|
53
|
+
/** The content types a file input should accept. */
|
|
54
|
+
accept: string;
|
|
55
|
+
}
|
|
56
|
+
export { ChatAttachmentsToolkit as n, ChatAttachmentDraft as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function uuid() {
|
|
2
|
+
const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
|
|
3
|
+
const time = Date.now();
|
|
4
|
+
for (let index = 0; index < 6; index++) bytes[index] = Math.floor(time / 2 ** (8 * (5 - index))) % 256;
|
|
5
|
+
bytes[6] = bytes[6] & 15 | 112;
|
|
6
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
7
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
8
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
9
|
+
}
|
|
10
|
+
const chatAttachmentContentTypes = [
|
|
11
|
+
"image/png",
|
|
12
|
+
"image/jpeg",
|
|
13
|
+
"image/webp",
|
|
14
|
+
"image/gif",
|
|
15
|
+
"application/pdf",
|
|
16
|
+
"text/plain",
|
|
17
|
+
"text/csv"
|
|
18
|
+
];
|
|
19
|
+
const chatAttachmentMaxByteSize = 10485760;
|
|
20
|
+
export { chatAttachmentMaxByteSize as n, uuid as r, chatAttachmentContentTypes as t };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AllowedComponentProps, ComponentCustomProps, VNodeProps } from "vue";
|
|
2
|
+
/**
|
|
3
|
+
* The type of each component in this package: the props it accepts and the slots
|
|
4
|
+
* it renders, which is what a template or `h()` checks.
|
|
5
|
+
*/
|
|
6
|
+
type PublicComponent<Props, Slots = object> = new () => {
|
|
7
|
+
$props: AllowedComponentProps & ComponentCustomProps & VNodeProps & Props;
|
|
8
|
+
$slots: Slots;
|
|
9
|
+
};
|
|
10
|
+
export { PublicComponent as t };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { computed, inject, onMounted, onScopeDispose, shallowRef } from "vue";
|
|
2
|
+
const dropByKey = Symbol("dropby-core");
|
|
3
|
+
function useStore(store, selector, isEqual = Object.is) {
|
|
4
|
+
const snapshot = shallowRef(store.getSnapshot());
|
|
5
|
+
let unsubscribe;
|
|
6
|
+
onMounted(() => {
|
|
7
|
+
const sync = () => {
|
|
8
|
+
snapshot.value = store.getSnapshot();
|
|
9
|
+
};
|
|
10
|
+
sync();
|
|
11
|
+
unsubscribe = store.subscribe(sync);
|
|
12
|
+
});
|
|
13
|
+
onScopeDispose(() => unsubscribe?.());
|
|
14
|
+
return computed((previous) => {
|
|
15
|
+
const next = selector(snapshot.value);
|
|
16
|
+
return previous !== void 0 && isEqual(previous, next) ? previous : next;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const identity = (value) => value;
|
|
20
|
+
function useDropBy() {
|
|
21
|
+
const dropby = inject(dropByKey, null);
|
|
22
|
+
if (dropby === null) throw new Error("DropBy composables must be used within a <DropByProvider>.");
|
|
23
|
+
return dropby;
|
|
24
|
+
}
|
|
25
|
+
function useDropByState() {
|
|
26
|
+
const core = useDropBy();
|
|
27
|
+
return {
|
|
28
|
+
connection: useStore(core.connection, identity),
|
|
29
|
+
capabilities: useStore(core.capabilities, identity),
|
|
30
|
+
bootstrap: useStore(core.bootstrap, identity)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export { dropByKey as i, useDropByState as n, useStore as r, useDropBy as t };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { r as useStore, t as useDropBy } from "./use-drop-by-4B6MMMx1.js";
|
|
2
|
+
import { onMounted, onScopeDispose, toValue, watch } from "vue";
|
|
3
|
+
function useChatThreads() {
|
|
4
|
+
const core = useDropBy();
|
|
5
|
+
return {
|
|
6
|
+
data: useStore(core.chat, (state) => state.threads),
|
|
7
|
+
actions: core.chat
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function useChatThread(id) {
|
|
11
|
+
const chat = useDropBy().chat;
|
|
12
|
+
let release;
|
|
13
|
+
onMounted(() => {
|
|
14
|
+
watch(() => toValue(id), (current) => {
|
|
15
|
+
release?.();
|
|
16
|
+
release = chat.observeThread(current);
|
|
17
|
+
}, { immediate: true });
|
|
18
|
+
});
|
|
19
|
+
onScopeDispose(() => release?.());
|
|
20
|
+
return {
|
|
21
|
+
data: useStore(chat, (state) => state.threadsById[toValue(id)]),
|
|
22
|
+
actions: {
|
|
23
|
+
open: () => chat.openThread(toValue(id)),
|
|
24
|
+
markSeen: (request) => chat.markThreadSeen(toValue(id), request),
|
|
25
|
+
loadOlderMessages: () => chat.loadOlderMessages(toValue(id)),
|
|
26
|
+
loadLatestMessages: () => chat.loadLatestMessages(toValue(id)),
|
|
27
|
+
sendMessage: (input, options) => chat.sendMessage(toValue(id), input, options),
|
|
28
|
+
getAttachmentDownload: (attachmentId) => chat.getAttachmentDownload(attachmentId)
|
|
29
|
+
},
|
|
30
|
+
olderMessages: useStore(chat, (state) => state.olderMessagesById[toValue(id)]),
|
|
31
|
+
latestMessages: useStore(chat, (state) => state.latestMessagesById[toValue(id)])
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function useChatUnread() {
|
|
35
|
+
const core = useDropBy();
|
|
36
|
+
return {
|
|
37
|
+
data: useStore(core.chat, (state) => state.unread),
|
|
38
|
+
actions: core.chat
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function useIdeas(selector, isEqual) {
|
|
42
|
+
const core = useDropBy();
|
|
43
|
+
if (selector) return {
|
|
44
|
+
data: useStore(core.ideas, selector, isEqual),
|
|
45
|
+
actions: core.ideas
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
data: useStore(core.ideas, (state) => state.ideas),
|
|
49
|
+
actions: core.ideas
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function usePendingAsks() {
|
|
53
|
+
const core = useDropBy();
|
|
54
|
+
return {
|
|
55
|
+
data: useStore(core.asks, (state) => state.pending),
|
|
56
|
+
actions: core.asks
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const EMPTY_CONFIG = Object.freeze({});
|
|
60
|
+
function useFeatureConfig() {
|
|
61
|
+
const core = useDropBy();
|
|
62
|
+
return useStore(core.bootstrap, (state) => state.data?.config ?? EMPTY_CONFIG);
|
|
63
|
+
}
|
|
64
|
+
function useFeatureFlag(key) {
|
|
65
|
+
const core = useDropBy();
|
|
66
|
+
return useStore(core.bootstrap, (state) => state.data?.config?.[toValue(key)]);
|
|
67
|
+
}
|
|
68
|
+
export { useChatThread as a, useIdeas as i, useFeatureFlag as n, useChatThreads as o, usePendingAsks as r, useChatUnread as s, useFeatureConfig as t };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { n as ChatAttachmentsToolkit, t as ChatAttachmentDraft } from "../../chunks/chat-attachment-types-BG8VFQj5.js";
|
|
2
|
+
/**
|
|
3
|
+
* Attachments for a `ChatComposer`: add files, see each upload and its preview,
|
|
4
|
+
* remove one, and hand the ready ones to the next message. Up to four files per
|
|
5
|
+
* message. The toolkit is reactive; unmounting the component releases the previews,
|
|
6
|
+
* and destroying the instance cancels the uploads.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useChatAttachments(): ChatAttachmentsToolkit;
|
|
9
|
+
export type { ChatAttachmentDraft, ChatAttachmentsToolkit };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { t as useDropBy } from "../../chunks/use-drop-by-4B6MMMx1.js";
|
|
2
|
+
import { r as uuid, t as chatAttachmentContentTypes } from "../../chunks/chat-attachments-BeJa4tEd.js";
|
|
3
|
+
import { computed, onScopeDispose, reactive, ref, shallowRef } from "vue";
|
|
4
|
+
import { createChatAttachment } from "@dropby/browser/attachments";
|
|
5
|
+
const accept = chatAttachmentContentTypes.join(",");
|
|
6
|
+
function useChatAttachments() {
|
|
7
|
+
const core = useDropBy();
|
|
8
|
+
const entries = /* @__PURE__ */ new Map();
|
|
9
|
+
const unsubscribes = /* @__PURE__ */ new Map();
|
|
10
|
+
const drafts = shallowRef([]);
|
|
11
|
+
const notice = ref(null);
|
|
12
|
+
function snapshot() {
|
|
13
|
+
const next = [];
|
|
14
|
+
for (const entry of entries.values()) {
|
|
15
|
+
const state = entry.upload.getSnapshot();
|
|
16
|
+
next.push({
|
|
17
|
+
localId: entry.localId,
|
|
18
|
+
fileName: state.fileName,
|
|
19
|
+
contentType: state.contentType,
|
|
20
|
+
byteSize: state.byteSize,
|
|
21
|
+
kind: state.kind,
|
|
22
|
+
previewUrl: entry.previewUrl,
|
|
23
|
+
status: state.status,
|
|
24
|
+
attachmentId: state.attachmentId,
|
|
25
|
+
error: state.error
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
drafts.value = next;
|
|
29
|
+
}
|
|
30
|
+
function add(files) {
|
|
31
|
+
const incoming = Array.from(files);
|
|
32
|
+
if (incoming.length === 0) return;
|
|
33
|
+
const availableSlots = 4 - entries.size;
|
|
34
|
+
if (availableSlots <= 0) {
|
|
35
|
+
notice.value = `you can attach up to 4 files.`;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const selected = incoming.slice(0, availableSlots);
|
|
39
|
+
notice.value = selected.length < incoming.length ? `only 4 attachments can be sent at once.` : null;
|
|
40
|
+
for (const file of selected) {
|
|
41
|
+
const localId = uuid();
|
|
42
|
+
const upload = createChatAttachment(core, file);
|
|
43
|
+
const previewUrl = file.type.startsWith("image/") ? URL.createObjectURL(file) : null;
|
|
44
|
+
entries.set(localId, {
|
|
45
|
+
localId,
|
|
46
|
+
upload,
|
|
47
|
+
previewUrl
|
|
48
|
+
});
|
|
49
|
+
unsubscribes.set(localId, upload.subscribe(snapshot));
|
|
50
|
+
upload.result.catch(() => void 0);
|
|
51
|
+
}
|
|
52
|
+
snapshot();
|
|
53
|
+
}
|
|
54
|
+
function remove(localId) {
|
|
55
|
+
const entry = entries.get(localId);
|
|
56
|
+
if (entry?.previewUrl) URL.revokeObjectURL(entry.previewUrl);
|
|
57
|
+
unsubscribes.get(localId)?.();
|
|
58
|
+
unsubscribes.delete(localId);
|
|
59
|
+
entries.delete(localId);
|
|
60
|
+
notice.value = null;
|
|
61
|
+
snapshot();
|
|
62
|
+
}
|
|
63
|
+
function clear() {
|
|
64
|
+
for (const localId of [...entries.keys()]) remove(localId);
|
|
65
|
+
}
|
|
66
|
+
function references() {
|
|
67
|
+
return drafts.value.flatMap((draft) => draft.status === "ready" && draft.attachmentId ? [{ attachmentId: draft.attachmentId }] : []);
|
|
68
|
+
}
|
|
69
|
+
const pending = computed(() => drafts.value.some((draft) => draft.status === "checking" || draft.status === "uploading"));
|
|
70
|
+
onScopeDispose(() => {
|
|
71
|
+
for (const unsubscribe of unsubscribes.values()) unsubscribe();
|
|
72
|
+
for (const entry of entries.values()) if (entry.previewUrl) URL.revokeObjectURL(entry.previewUrl);
|
|
73
|
+
unsubscribes.clear();
|
|
74
|
+
entries.clear();
|
|
75
|
+
});
|
|
76
|
+
return reactive({
|
|
77
|
+
drafts,
|
|
78
|
+
add,
|
|
79
|
+
remove,
|
|
80
|
+
clear,
|
|
81
|
+
references,
|
|
82
|
+
pending,
|
|
83
|
+
notice,
|
|
84
|
+
accept
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export { useChatAttachments };
|