diff --git a/package.json b/package.json index e1c5d4a14..a4c025376 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,6 @@ "uuid": "^9.0.1", "vite-plugin-node-polyfills": "^0.21.0", "voice-activity-detection": "^0.0.5", - "warp-wasm": "1.6.2" + "warp-wasm": "^1.7.1" } } diff --git a/src/lib/layouts/Chatbar.svelte b/src/lib/layouts/Chatbar.svelte index 38138e935..8486e5f49 100644 --- a/src/lib/layouts/Chatbar.svelte +++ b/src/lib/layouts/Chatbar.svelte @@ -98,7 +98,6 @@ UIStore.mutateChat(chat.id, c => { c.last_view_date = new Date() }) - ConversationStore.addPendingMessages(chat.id, res.message, txt) }) if (!isStickerOrGif) { chatMessages.update(messages => { diff --git a/src/lib/wasm/CommunitiesStore.ts b/src/lib/wasm/CommunitiesStore.ts new file mode 100644 index 000000000..cd8cd920f --- /dev/null +++ b/src/lib/wasm/CommunitiesStore.ts @@ -0,0 +1,355 @@ +import { get, type Writable } from "svelte/store" +import * as wasm from "warp-wasm" +import { WarpStore } from "./WarpStore" +import { failure, success, type Result } from "$lib/utils/Result" +import { handleErrors, WarpError } from "./HandleWarpErrors" +import { MessageOptions } from "warp-wasm" +import { convertWarpAttachment, createFileAttachHandler, createFileDownloadHandler, createFileDownloadHandlerRaw, type FetchMessageResponse, type FetchMessagesConfig, type FileAttachment, type SendMessageResult } from "./RaygunStore" +import { messageTypeFromTexts, type Message, type Reaction } from "$lib/types" +import { MultipassStoreInstance } from "./MultipassStore" +import { Store } from "$lib/state/Store" +import { Appearance } from "$lib/enums" + +class CommunitiesStore { + private raygunWritable: Writable + + constructor(raygun: Writable) { + this.raygunWritable = raygun + } + + async createCommunity(name: string) { + return this.get(r => r.create_community(name), `Error creating a community with name ${name}`) + } + async deleteCommunity(community_id: string) { + return this.get(r => r.delete_community(community_id), `Error deleting community ${community_id}`) + } + async getCommunity(community_id: string) { + return this.get(r => r.get_community(community_id), `Error getting community ${community_id}`) + } + async listCommunitiesJoined() { + return this.get(r => r.list_communities_joined(), `Error listing joined communities`) + } + async listCommunitiesInvitedTo() { + return this.get(r => r.list_communities_invited_to(), `Error listing community invitations`) + } + async leaveCommunity(community_id: string) { + return this.get(r => r.leave_community(community_id), `Error leaving community ${community_id}`) + } + + async getCommunityIcon(community_id: string) { + return this.get(r => r.get_community_icon(community_id), `Error getting icon for community ${community_id}`) + } + async getCommunityBanner(community_id: string) { + return this.get(r => r.get_community_banner(community_id), `Error getting banner for community ${community_id}`) + } + async editCommunityPicture(community_id: string, picture: string, banner?: boolean) { + return this.get(r => { + const data = picture.startsWith("data:") ? picture.split(",")[1] : picture + const buffer = Buffer.from(data, "base64") + const len = buffer.length + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(buffer) + controller.close() + }, + }) + let file = new wasm.AttachmentFile("", new wasm.AttachmentStream(len, stream)) + return banner ? r.edit_community_banner(community_id, file) : r.edit_community_icon(community_id, file) + }, `Error editing picture for community ${community_id}`) + } + + async createCommunityInvite(community_id: string, target_user?: string, expiry?: Date) { + return this.get(r => r.create_community_invite(community_id, target_user, expiry), `Error creating invite for community ${community_id}`) + } + async deleteCommunityInvite(community_id: string, invite_id: string) { + return this.get(r => r.delete_community_invite(community_id, invite_id), `Error deleting invite ${invite_id} for community ${community_id}`) + } + async getCommunityInvite(community_id: string, invite_id: string) { + return this.get(r => r.get_community_invite(community_id, invite_id), `Error getting invite ${invite_id} for community ${community_id}`) + } + async acceptCommunityInvite(community_id: string, invite_id: string) { + return this.get(r => r.accept_community_invite(community_id, invite_id), `Error accepting invite ${invite_id} for community ${community_id}`) + } + async editCommunityInvite(community_id: string, invite_id: string, updater: (inv: wasm.CommunityInvite) => void) { + return this.get(async r => { + let invite = await r.get_community_invite(community_id, invite_id) + updater(invite) + return r.edit_community_invite(community_id, invite_id, invite) + }, `Error editing invite ${invite_id} for community ${community_id}`) + } + + async createCommunityRole(community_id: string, name: string) { + return this.get(r => r.create_community_role(community_id, name), `Error creating role for community ${community_id}`) + } + async deleteCommunityRole(community_id: string, role_id: string) { + return this.get(r => r.delete_community_role(community_id, role_id), `Error deleting role for community ${community_id}`) + } + async getCommunityRole(community_id: string, role_id: string) { + return this.get(r => r.get_community_role(community_id, role_id), `Error getting role for community ${community_id}`) + } + async editCommunityRoleName(community_id: string, role_id: string, new_name: string) { + return this.get(r => r.edit_community_role_name(community_id, role_id, new_name), `Error editing role ${role_id} for community ${community_id}`) + } + async grantCommunityRole(community_id: string, role_id: string, user: string) { + return this.get(r => r.grant_community_role(community_id, role_id, user), `Error granting role ${role_id} in community ${community_id} for user ${user}`) + } + async revokeCommunityRole(community_id: string, role_id: string, user: string) { + return this.get(r => r.revoke_community_role(community_id, role_id, user), `Error revoking role ${role_id} in community ${community_id} from user ${user}`) + } + + async createCommunityChannel(community_id: string, channel_name: string, channel_type: wasm.CommunityChannelType) { + return this.get(r => r.create_community_channel(community_id, channel_name, channel_type), `Error creating channel for community ${community_id}`) + } + async deleteCommunityChannel(community_id: string, channel_id: string) { + return this.get(r => r.delete_community_channel(community_id, channel_id), `Error deleting channel ${channel_id} for community ${community_id}`) + } + async getCommunityChannel(community_id: string, channel_id: string) { + return this.get(r => r.get_community_channel(community_id, channel_id), `Error getting channel for community ${community_id}`) + } + + async editCommunityName(community_id: string, name: string) { + return this.get(r => r.edit_community_name(community_id, name), `Error editing name for community ${community_id}`) + } + async editCommunityDescription(community_id: string, description?: string) { + return this.get(r => r.edit_community_description(community_id, description), `Error editing description for community ${community_id}`) + } + + async grantCommunityPermission(community_id: string, permission: CommunityPermission, role_id: string) { + return this.get(r => r.grant_community_permission(community_id, permission, role_id), `Error granting permission to role ${role_id} for community ${community_id}`) + } + async revokeCommunityPermission(community_id: string, permission: CommunityPermission, role_id: string) { + return this.get(r => r.revoke_community_permission(community_id, permission, role_id), `Error revoking permission for role ${role_id} in community ${community_id}`) + } + async grantCommunityPermissionForAll(community_id: string, permission: CommunityPermission) { + return this.get(r => r.grant_community_permission_for_all(community_id, permission), `Error granting permission for all in community ${community_id}`) + } + async revokeCommunityPermissionForAll(community_id: string, permission: CommunityPermission) { + return this.get(r => r.revoke_community_permission_for_all(community_id, permission), `Error revoking permission for all in community ${community_id}`) + } + + async removeCommunityMember(community_id: string, member: string) { + return this.get(r => r.remove_community_member(community_id, member), `Error removing member ${member} from community ${community_id}`) + } + + async editCommunityChannelName(community_id: string, channel_id: string, name: string) { + return this.get(r => r.edit_community_channel_name(community_id, channel_id, name), `Error editing channel ${channel_id} name for community ${community_id}`) + } + async editCommunityChannelDescription(community_id: string, channel_id: string, description?: string) { + return this.get(r => r.edit_community_channel_description(community_id, channel_id, description), `Error editing channel ${channel_id} description for community ${community_id}`) + } + + async grantCommunityChannelPermission(community_id: string, channel_id: string, permission: CommunityChannelPermission, role_id: string) { + return this.get(r => r.grant_community_channel_permission(community_id, channel_id, permission, role_id), `Error granting channel permission for role ${role_id} in community ${community_id}`) + } + async revokeCommunityChannelPermission(community_id: string, channel_id: string, permission: CommunityChannelPermission, role_id: string) { + return this.get(r => r.revoke_community_channel_permission(community_id, channel_id, permission, role_id), `Error revoking channel permission for role ${role_id} in for community ${community_id}`) + } + async grantCommunityChannelPermissionForAll(community_id: string, channel_id: string, permission: CommunityChannelPermission) { + return this.get(r => r.grant_community_channel_permission_for_all(community_id, channel_id, permission), `Error granting channel permission for community ${community_id}`) + } + async revokeCommunityChannelPermissionForAll(community_id: string, channel_id: string, permission: CommunityChannelPermission) { + return this.get(r => r.revoke_community_channel_permission_for_all(community_id, channel_id, permission), `Error revoking channel permission for community ${community_id}`) + } + + async getCommunityChannelMessage(community_id: string, channel_id: string, message_id: string) { + return this.get(r => r.get_community_channel_message(community_id, channel_id, message_id), `Error revoking channel permission for community ${community_id}`) + } + + async fetchCommunityMessages(community_id: string, channel_id: string, config: FetchMessagesConfig): Promise> { + return this.get(async r => { + let message_options = new MessageOptions() + switch (config.type) { + case "Between": { + message_options.set_date_range(config.from, config.to) //TODO verify that js Date can be parsed to rust DateTime:: + break + } + case "MostRecent": { + let total_messages = await r.get_community_channel_message_count(community_id, channel_id) + message_options.set_range(Math.min(0, total_messages - config.amount), total_messages) + break + } + case "Earlier": { + message_options.set_date_range(new Date(), config.start_date) + message_options.set_reverse() + message_options.set_limit(config.limit) + break + } + case "Later": { + message_options.set_date_range(config.start_date, new Date()) + message_options.set_limit(config.limit) + break + } + } + + let messages = await this.getMessages(r, community_id, channel_id, message_options) + if (config.type === "Earlier") { + messages = messages.reverse() + } + let has_more = "limit" in config ? messages.length >= config.limit : false + + let opt = new MessageOptions() + opt.set_limit(1) + opt.set_last_message() + let most_recent = await this.getMessages(r, community_id, channel_id, opt) + return { + messages: messages, + has_more: has_more, + most_recent: most_recent[most_recent.length].id, + } + }, "Error fetching messages") + } + + private async getMessages(raygun: wasm.RayGunBox, community_id: string, channel_id: string, options: MessageOptions) { + let msgs = await raygun.get_community_channel_messages(community_id, channel_id, options) + let messages: Message[] = [] + if (msgs.variant() === wasm.MessagesEnum.List) { + let warpMsgs = msgs.messages()! + messages = (await Promise.all(warpMsgs.map(async msg => await this.convertCommunityMessage(community_id, channel_id, msg)))).filter((m: Message | null): m is Message => m !== null) + } + return messages + } + + async communityChannelMessageStatus(community_id: string, channel_id: string, message_id: string) { + return this.get(r => r.community_channel_message_status(community_id, channel_id, message_id), `Error fetching message status for message ${message_id} in community ${community_id}`) + } + async sendCommunityChannelMessage(community_id: string, channel_id: string, message: string[], attachments?: FileAttachment[]) { + return await this.get(async r => { + return await this.sendTo(r, community_id, channel_id, message, { attachments }) + }, `Error sending message in channel ${channel_id} for community ${community_id}`) + } + async editCommunityChannelMessage(community_id: string, channel_id: string, message_id: string, message: string[]) { + return this.get(r => r.edit_community_channel_message(community_id, channel_id, message_id, message), `Error revoking channel permission for community ${community_id}`) + } + async replyToCommunityChannelMessage(community_id: string, channel_id: string, message_id: string, message: string[], attachments?: FileAttachment[]) { + return await this.get(async r => { + return await this.sendTo(r, community_id, channel_id, message, { attachments, replyTo: message_id }) + }, `Error replying to message ${message_id} in channel ${channel_id} for community ${community_id}`) + } + async deleteCommunityChannelMessage(community_id: string, channel_id: string, message_id: string) { + return this.get(r => r.delete_community_channel_message(community_id, channel_id, message_id), `Error deleting message ${message_id} for channel ${channel_id} in community ${community_id}`) + } + async pinCommunityChannelMessage(community_id: string, channel_id: string, message_id: string, pin: boolean) { + return this.get(r => r.pin_community_channel_message(community_id, channel_id, message_id, pin ? wasm.PinState.Pin : wasm.PinState.Unpin), `Error pinning message for community ${community_id}`) + } + async reactToCommunityChannelMessage(community_id: string, channel_id: string, message_id: string, state: wasm.ReactionState, emoji: string) { + let result = await this.get(r => r.react_to_community_channel_message(community_id, channel_id, message_id, state, emoji), "Error reacting to community message") + return result.map(_ => { + // TODO: Requires Store changes + // ConversationStore.editReaction(get(Store.state.activeChat).id, message_id, emoji, state == wasm.ReactionState.Add) + }) + } + + async downloadFromCommunityChannelMessage(community_id: string, channel_id: string, message_id: string, file: string, size?: number) { + return await this.get(async r => { + let result = await r.download_stream_from_community_channel_message(community_id, channel_id, message_id, file) + return createFileDownloadHandler(file, result, size) + }, `Error downloading community attachment from community ${community_id} for message ${message_id}`) + } + + async getCommunityMessageAttachmentRaw(community_id: string, channel_id: string, message_id: string, file: string, options?: { size?: number; type?: string }) { + return await this.get(async r => { + let result = await r.download_stream_from_community_channel_message(community_id, channel_id, message_id, file) + return createFileDownloadHandlerRaw(file, result, options) + }, `Error downloading community attachment from community ${community_id} for message ${message_id}`) + } + + async sendCommunityChannelMesssageEvent(community_id: string, channel_id: string, event: wasm.MessageEvent) { + return await this.get(r => r.send_community_channel_messsage_event(community_id, channel_id, event), `Error sending event ${event}`) + } + async cancelCommunityChannelMesssageEvent(community_id: string, channel_id: string, event: wasm.MessageEvent) { + return await this.get(r => r.cancel_community_channel_messsage_event(community_id, channel_id, event), `Error sending event ${event}`) + } + + private async sendTo(raygun: wasm.RayGunBox, community_id: string, channel_id: string, message: string[], settings?: { attachments?: FileAttachment[]; replyTo?: string }): Promise { + if (settings?.attachments && settings?.attachments.length > 0) { + let result = await raygun + .attach_to_community_channel_message( + community_id, + channel_id, + settings?.replyTo, + settings?.attachments.map(f => new wasm.AttachmentFile(f.file, f.attachment ? new wasm.AttachmentStream(f.attachment[1], f.attachment[0]) : undefined)), + message + ) + .then(res => { + // message_sent event gets fired AFTER this returns + // TODO: Requires Store changes + // ConversationStore.addPendingMessages(conversation_id, res.get_message_id(), message) + createFileAttachHandler(res, (file, updater) => { + //TODO: Update file on store + }) + return res + }) + return { + message: result.get_message_id(), + progress: result, + } + } + return { + message: await (settings?.replyTo ? raygun.reply_to_community_channel_message(community_id, channel_id, settings.replyTo, message) : raygun.send_community_channel_message(community_id, channel_id, message)).then(messageId => { + // message_sent event gets fired BEFORE this returns + // So to + // 1. unify this system + // 2. keep it roughly the same as native (as on native due to some channel delays it handles message_sent after #send returns) + // We add the pending msg here and remove it in message_sent which has a short delay + // TODO: Requires Store changes + // ConversationStore.addPendingMessages(conversation_id, messageId, message) + return messageId + }), + } + } + + /** + * Convenient helper method to get data from raygun + */ + private async get(handler: (raygun: wasm.RayGunBox) => Promise | T, err: string): Promise> { + let raygun = get(this.raygunWritable) + if (raygun) { + try { + return success(await handler(raygun)) + } catch (error) { + return failure(handleErrors(`${err}: ${error}`)) + } + } + return failure(WarpError.RAYGUN_NOT_FOUND) + } + + private async convertCommunityMessage(community_id: string, channel_id: string, message: wasm.Message | undefined): Promise { + if (!message) return null + let user = get(Store.state.user) + let remote = message.sender() !== user.key + if (remote) { + let sender = await MultipassStoreInstance.identity_from_did(message.sender()) + if (sender) Store.updateUser(sender) + } + let attachments = message.attachments() + let reactions: { [key: string]: Reaction } = {} + message.reactions().forEach((dids, emoji) => { + reactions[emoji] = { + reactors: new Set(dids), + emoji: emoji, + highlight: Appearance.Default, //TODO + description: "", //TODO + } + }) + return { + id: message.id(), + details: { + at: message.date(), + origin: message.sender(), + remote: remote, + }, + text: message.lines(), + inReplyTo: message.replied() ? null : null, // fetch from ConversationStore + reactions: reactions, + attachments: attachments.map(f => convertWarpAttachment(f)), + pinned: message.pinned(), + type: messageTypeFromTexts(message.lines()), + } + } +} + +export type CommunityPermission = wasm.CommunityPermission +export const CommunityPermission = wasm.CommunityPermission +export type CommunityChannelPermission = wasm.CommunityChannelPermission +export const CommunityChannelPermission = wasm.CommunityChannelPermission +export const CommunitiesStoreInstance = new CommunitiesStore(WarpStore.warp.raygun) diff --git a/src/lib/wasm/RaygunStore.ts b/src/lib/wasm/RaygunStore.ts index a6658ec8a..26e6d24b4 100644 --- a/src/lib/wasm/RaygunStore.ts +++ b/src/lib/wasm/RaygunStore.ts @@ -6,7 +6,7 @@ import { UIStore } from "../state/ui" import { ConversationStore } from "../state/conversation" import { MessageOptions } from "warp-wasm" import { Appearance, ChatType, MessageAttachmentKind, Route } from "$lib/enums" -import { type User, type Chat, defaultChat, type Message, mentions_user, type Attachment, messageTypeFromTexts, type Reaction } from "$lib/types" +import { type User, type Chat, defaultChat, type Message, mentions_user, type Attachment, messageTypeFromTexts, type Reaction, type FileProgress } from "$lib/types" import { WarpError, handleErrors } from "./HandleWarpErrors" import { failure, success, type Result } from "$lib/utils/Result" import { create_cancellable_handler, type Cancellable } from "$lib/utils/CancellablePromise" @@ -212,7 +212,7 @@ class RaygunStore { }, }) let file = new wasm.AttachmentFile("", new wasm.AttachmentStream(len, stream)) - banner ? r.update_conversation_banner(conversation_id, file) : r.update_conversation_icon(conversation_id, file) + return banner ? r.update_conversation_banner(conversation_id, file) : r.update_conversation_icon(conversation_id, file) }, "Error updating conversation icon") } @@ -306,7 +306,7 @@ class RaygunStore { async send(conversation_id: string, message: string[], attachments?: FileAttachment[]): Promise> { return await this.get(async r => { - return await this.sendTo(r, conversation_id, message, attachments) + return await this.sendTo(r, conversation_id, message, { attachments }) }, "Error sending message") } @@ -314,26 +314,28 @@ class RaygunStore { return await this.get(async r => { let sent = [] for (let conversation_id of conversation_ids) { - let res: MultiSendMessageResult = { chat: conversation_id, result: await this.sendTo(r, conversation_id, message, attachments) } + let res: MultiSendMessageResult = { chat: conversation_id, result: await this.sendTo(r, conversation_id, message, { attachments }) } sent.push(res) } return sent }, "Error sending message") } - private async sendTo(raygun: wasm.RayGunBox, conversation_id: string, message: string[], attachments?: FileAttachment[]): Promise { - if (attachments && attachments.length > 0) { + private async sendTo(raygun: wasm.RayGunBox, conversation_id: string, message: string[], settings?: { attachments?: FileAttachment[]; replyTo?: string }): Promise { + if (settings?.attachments && settings?.attachments.length > 0) { + // Check for empty messages + message = message.filter(m => m.length !== 0).length === 0 ? [] : message let result = await raygun .attach( conversation_id, - undefined, - attachments.map(f => new wasm.AttachmentFile(f.file, f.attachment ? new wasm.AttachmentStream(f.attachment[1], f.attachment[0]) : undefined)), + settings?.replyTo, + settings?.attachments.map(f => new wasm.AttachmentFile(f.file, f.attachment ? new wasm.AttachmentStream(f.attachment[1], f.attachment[0]) : undefined)), message ) .then(res => { // message_sent event gets fired AFTER this returns ConversationStore.addPendingMessages(conversation_id, res.get_message_id(), message) - this.createFileAttachHandler(conversation_id, res) + createFileAttachHandler(res, (file, updater) => ConversationStore.updatePendingMessages(conversation_id, res.get_message_id(), file, updater)) return res }) return { @@ -342,7 +344,7 @@ class RaygunStore { } } return { - message: await raygun.send(conversation_id, message).then(messageId => { + message: await (settings?.replyTo ? raygun.reply(conversation_id, settings.replyTo, message) : raygun.send(conversation_id, message)).then(messageId => { // message_sent event gets fired BEFORE this returns // So to // 1. unify this system @@ -361,14 +363,14 @@ class RaygunStore { async downloadAttachment(conversation_id: string, message_id: string, file: string, size?: number) { return await this.get(async r => { let result = await r.download_stream(conversation_id, message_id, file) - return this.createFileDownloadHandler(file, result, size) + return createFileDownloadHandler(file, result, size) }, `Error downloading attachment from ${conversation_id} for message ${message_id}`) } async getAttachmentRaw(conversation_id: string, message_id: string, file: string, options?: { size?: number; type?: string }) { return await this.get(async r => { let result = await r.download_stream(conversation_id, message_id, file) - return this.createFileDownloadHandlerRaw(file, result, options) + return createFileDownloadHandlerRaw(file, result, options) }, `Error downloading attachment from ${conversation_id} for message ${message_id}`) } @@ -385,28 +387,18 @@ class RaygunStore { async reply(conversation_id: string, message_id: string, message: string[], attachments?: FileAttachment[]): Promise> { return await this.get(async r => { - if (attachments && attachments.length > 0) { - let result = await r.attach( - conversation_id, - message_id, - attachments.map(f => new wasm.AttachmentFile(f.file, f.attachment ? new wasm.AttachmentStream(f.attachment[1], f.attachment[0]) : undefined)), - message - ) - return { - message: result.get_message_id(), - progress: result, - } - } - return { - message: await r.reply(conversation_id, message_id, message), - } - }, "Error replying to message") + return await this.sendTo(r, conversation_id, message, { attachments, replyTo: message_id }) + }, "Error sending message") } async sendEvent(conversation_id: string, event: wasm.MessageEvent) { return await this.get(r => r.send_event(conversation_id, event), `Error sending event ${event}`) } + async cancelEvent(conversation_id: string, event: wasm.MessageEvent) { + return await this.get(r => r.cancel_event(conversation_id, event), `Error cancelling event ${event}`) + } + private async handleRaygunEvent(raygun: wasm.RayGunBox) { let events: wasm.AsyncIterator | undefined while (!events) { @@ -436,7 +428,7 @@ class RaygunStore { let conv = await raygun.get_conversation(conversationId) let chat = await this.convertWarpConversation(conv, raygun) let listeners = get(this.messageListeners) - let handler = await this.createConversationEventHandler(raygun, conversationId) + let handler = await this.createMessageEventHandler(raygun, conversationId) listeners[conversationId] = handler this.messageListeners.set(listeners) @@ -464,6 +456,35 @@ class RaygunStore { } break } + case "conversation_archived": + case "conversation_unarchived": { + //TODO + break + } + case "community_created": { + //TODO UI stuff + let communityId: string = event.values["community_id"] + let listeners = get(this.messageListeners) + let handler = await this.createMessageEventHandler(raygun, communityId, true) + listeners[communityId] = handler + this.messageListeners.set(listeners) + + break + } + case "community_deleted": { + let communityId: string = event.values["community_id"] + let listeners = get(this.messageListeners) + if (communityId in listeners) { + listeners[communityId].cancel() + delete listeners[communityId] + this.messageListeners.set(listeners) + } + //TODO UI stuff + break + } + case "community_invited": { + break + } } } } @@ -487,14 +508,19 @@ class RaygunStore { } let handlers: { [key: string]: Cancellable } = {} for (let conversation of conversations.convs()) { - let handler = await this.createConversationEventHandler(raygun, conversation.id()) + let handler = await this.createMessageEventHandler(raygun, conversation.id()) handlers[conversation.id()] = handler } + let communities = await raygun.list_communities_joined() + for (let communitiy of communities) { + let handler = await this.createMessageEventHandler(raygun, communitiy, true) + handlers[communitiy] = handler + } this.messageListeners.set(handlers) } - private async createConversationEventHandler(raygun: wasm.RayGunBox, conversation_id: string) { - let stream = await raygun.get_conversation_stream(conversation_id) + private async createMessageEventHandler(raygun: wasm.RayGunBox, identifier: string, community?: boolean) { + let stream = community ? await raygun.get_community_stream(identifier) : await raygun.get_conversation_stream(identifier) return create_cancellable_handler(async isCancelled => { let listener = { [Symbol.asyncIterator]() { @@ -504,8 +530,12 @@ class RaygunStore { streamLoop: for await (const value of listener) { let event = parseJSValue(value) log.info(`Handling message event: ${JSON.stringify(event)}`) + if (community) { + //TODO UI Hookup etc not implemented + continue + } if (isCancelled()) { - log.debug(`Breaking stream loop not necessary anymore from: ${conversation_id}`) + log.debug(`Breaking stream loop not necessary anymore from: ${identifier}`) break streamLoop } switch (event.type) { @@ -702,109 +732,6 @@ class RaygunStore { return messages } - private async createFileDownloadHandlerRaw(name: string, it: wasm.AsyncIterator, options?: { size?: number; type?: string }): Promise { - let listener = { - [Symbol.asyncIterator]() { - return it - }, - } - let data: any[] = [] - try { - for await (const value of listener) { - data = [...data, ...value] - } - } catch (_) {} - return new File([new Uint8Array(data)], name, { type: options?.type }) - } - - private async createFileDownloadHandler(name: string, it: wasm.AsyncIterator, size?: number) { - let blob = await this.createFileDownloadHandlerRaw(name, it, { size }) - const elem = window.document.createElement("a") - elem.href = window.URL.createObjectURL(blob) - elem.download = name - document.body.appendChild(elem) - elem.click() - document.body.removeChild(elem) - } - - /** - * Create a handler for attachment results that uploads the file to chat and updates pending message attachments - * TODO: verify it works as we dont have a way to upload files yet - */ - private async createFileAttachHandler(conversationId: string, upload: wasm.AttachmentResult) { - let listener = { - [Symbol.asyncIterator]() { - return upload - }, - } - let cancelled = false - try { - for await (const value of listener) { - let event = parseJSValue(value) - log.info(`Handling file progress event: ${JSON.stringify(event)}`) - switch (event.type) { - case "AttachedProgress": { - let locationKind = parseJSValue(event.values[0]) - // Only streams need progress update - if (locationKind.type === "Stream") { - let progress = parseJSValue(event.values[1]) - let file = progress.values["name"] - ConversationStore.updatePendingMessages(conversationId, upload.get_message_id(), file, current => { - if (current) { - let copy = { ...current } - switch (progress.type) { - case "CurrentProgress": { - copy.size = progress.values["current"] - copy.total = progress.values["total"] - break - } - case "ProgressComplete": { - copy.size = progress.values["total"] - copy.total = progress.values["total"] - copy.done = true - break - } - case "ProgressFailed": { - copy.size = progress.values["last_size"] - copy.error = `Error: ${progress.values["error"]}` - break - } - } - return copy - } else if (progress.type === "CurrentProgress") { - return { - name: file, - size: progress.values["current"], - total: progress.values["total"], - cancellation: { - cancel: () => { - cancelled = true - }, - }, - } - } - return undefined - }) - } - break - } - case "Pending": { - if (Object.keys(event.values).length > 0) { - let res = parseJSValue(event.values) - if (res.type === "Err") { - log.error(`Error uploading file ${res.values}`) - } - } - break - } - } - if (cancelled) break - } - } catch (e) { - if (!`${e}`.includes(`Error: returned None`)) throw e - } - } - /** * Convenient helper method to get data from raygun */ @@ -851,35 +778,12 @@ class RaygunStore { text: message.lines(), inReplyTo: message.replied() ? ConversationStore.getMessage(conversation_id, message.replied()!) : null, reactions: reactions, - attachments: attachments.map(f => this.convertWarpAttachment(f)), + attachments: attachments.map(f => convertWarpAttachment(f)), pinned: message.pinned(), type: messageTypeFromTexts(message.lines()), } } - private convertWarpAttachment(attachment: wasm.File): Attachment { - let kind: MessageAttachmentKind = MessageAttachmentKind.File - let type = attachment.file_type() - let mime = "application/octet-stream" - if (type !== "Generic") { - mime = type.Mime - } - if (mime.startsWith("image")) { - kind = MessageAttachmentKind.Image - } else if (mime.startsWith("video")) { - kind = MessageAttachmentKind.Video - } - let thumbnail = attachment.thumbnail() - let location = thumbnail.length > 0 ? imageFromData(thumbnail, type) : "" - return { - kind: kind, - name: attachment.name(), - size: attachment.size(), - location: location, - mime: mime, - } - } - /** * Converts warp message to ui message */ @@ -916,6 +820,133 @@ class RaygunStore { } } +export async function createFileDownloadHandlerRaw(name: string, it: wasm.AsyncIterator, options?: { size?: number; type?: string }): Promise { + let listener = { + [Symbol.asyncIterator]() { + return it + }, + } + let data: any[] = [] + try { + for await (const value of listener) { + data = [...data, ...value] + } + } catch (_) {} + return new File([new Uint8Array(data)], name, { type: options?.type }) +} + +export async function createFileDownloadHandler(name: string, it: wasm.AsyncIterator, size?: number) { + let blob = await createFileDownloadHandlerRaw(name, it, { size }) + const elem = window.document.createElement("a") + elem.href = window.URL.createObjectURL(blob) + elem.download = name + document.body.appendChild(elem) + elem.click() + document.body.removeChild(elem) +} + +/** + * Create a handler for attachment results that uploads the file to chat and updates pending message attachments + * TODO: verify it works as we dont have a way to upload files yet + */ +export type ProgressHandler = (progress: FileProgress | undefined) => FileProgress | undefined +export async function createFileAttachHandler(upload: wasm.AttachmentResult, updater: (name: string, handler: ProgressHandler) => void) { + let listener = { + [Symbol.asyncIterator]() { + return upload + }, + } + let cancelled = false + try { + for await (const value of listener) { + let event = parseJSValue(value) + log.info(`Handling file progress event: ${JSON.stringify(event)}`) + switch (event.type) { + case "AttachedProgress": { + let locationKind = parseJSValue(event.values[0]) + // Only streams need progress update + if (locationKind.type === "Stream") { + let progress = parseJSValue(event.values[1]) + let file = progress.values["name"] + updater(file, current => { + if (current) { + let copy = { ...current } + switch (progress.type) { + case "CurrentProgress": { + copy.size = progress.values["current"] + copy.total = progress.values["total"] + break + } + case "ProgressComplete": { + copy.size = progress.values["total"] + copy.total = progress.values["total"] + copy.done = true + break + } + case "ProgressFailed": { + copy.size = progress.values["last_size"] + copy.error = `Error: ${progress.values["error"]}` + break + } + } + return copy + } else if (progress.type === "CurrentProgress") { + return { + name: file, + size: progress.values["current"], + total: progress.values["total"], + cancellation: { + cancel: () => { + cancelled = true + }, + }, + } + } + return undefined + }) + } + break + } + case "Pending": { + if (Object.keys(event.values).length > 0) { + let res = parseJSValue(event.values) + if (res.type === "Err") { + log.error(`Error uploading file ${res.values}`) + } + } + break + } + } + if (cancelled) break + } + } catch (e) { + if (!`${e}`.includes(`Error: returned None`)) throw e + } +} + +export function convertWarpAttachment(attachment: wasm.File): Attachment { + let kind: MessageAttachmentKind = MessageAttachmentKind.File + let type = attachment.file_type() + let mime = "application/octet-stream" + if (type !== "Generic") { + mime = type.Mime + } + if (mime.startsWith("image")) { + kind = MessageAttachmentKind.Image + } else if (mime.startsWith("video")) { + kind = MessageAttachmentKind.Video + } + let thumbnail = attachment.thumbnail() + let location = thumbnail.length > 0 ? imageFromData(thumbnail, type) : "" + return { + kind: kind, + name: attachment.name(), + size: attachment.size(), + location: location, + mime: mime, + } +} + export type GroupPermission = wasm.GroupPermission export const GroupPermission = wasm.GroupPermission export const RaygunStoreInstance = new RaygunStore(WarpStore.warp.raygun)