From 2a061d58ec7998c02e1cc1c32a83e6405a866a00 Mon Sep 17 00:00:00 2001
From: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
Date: Tue, 1 Mar 2022 20:14:51 +0600
Subject: [PATCH] Attachments in comments (#1077)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
---
 models/chunter/package.json                   |   1 +
 models/chunter/src/index.ts                   |   9 +-
 .../src/components/ReferenceInput.svelte      |  10 +-
 plugins/attachment-resources/package.json     |   1 +
 .../src/components/AttachmentDocList.svelte   |  41 +++++
 .../src/components/AttachmentList.svelte      |  47 ++++++
 .../src/components/AttachmentRefInput.svelte  | 156 ++++++++++++++++++
 plugins/attachment-resources/src/index.ts     |   5 +-
 plugins/chunter-resources/package.json        |   2 +
 .../src/components/Channel.svelte             |   3 +-
 .../src/components/ChannelView.svelte         |  19 ++-
 .../src/components/CommentInput.svelte        |  14 +-
 .../src/components/Message.svelte             |  11 +-
 .../activity/TxCommentCreate.svelte           |   2 +
 plugins/chunter/src/index.ts                  |   2 +
 15 files changed, 305 insertions(+), 18 deletions(-)
 create mode 100644 plugins/attachment-resources/src/components/AttachmentDocList.svelte
 create mode 100644 plugins/attachment-resources/src/components/AttachmentList.svelte
 create mode 100644 plugins/attachment-resources/src/components/AttachmentRefInput.svelte

diff --git a/models/chunter/package.json b/models/chunter/package.json
index 95350ee8e1..0966f060ce 100644
--- a/models/chunter/package.json
+++ b/models/chunter/package.json
@@ -29,6 +29,7 @@
     "@anticrm/model": "~0.6.0",
     "@anticrm/ui": "~0.6.0",
     "@anticrm/view": "~0.6.0",
+    "@anticrm/model-attachment": "~0.6.0",
     "@anticrm/chunter": "~0.6.0",
     "@anticrm/chunter-resources": "~0.6.0",
     "@anticrm/platform": "~0.6.5",
diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts
index 88db467d51..f75c815df5 100644
--- a/models/chunter/src/index.ts
+++ b/models/chunter/src/index.ts
@@ -17,12 +17,13 @@ import activity from '@anticrm/activity'
 import type { Backlink, Channel, Comment, Message } from '@anticrm/chunter'
 import type { Class, Doc, Domain, Ref } from '@anticrm/core'
 import { IndexKind } from '@anticrm/core'
-import { Builder, Index, Model, Prop, TypeMarkup, UX } from '@anticrm/model'
+import { Builder, Collection, Index, Model, Prop, TypeMarkup, UX } from '@anticrm/model'
 import core, { TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
 import view from '@anticrm/model-view'
 import workbench from '@anticrm/model-workbench'
 import { ObjectDDParticipant } from '@anticrm/view'
 import chunter from './plugin'
+import attachment from '@anticrm/model-attachment'
 
 export const DOMAIN_CHUNTER = 'chunter' as Domain
 export const DOMAIN_COMMENT = 'comment' as Domain
@@ -36,6 +37,9 @@ export class TMessage extends TDoc implements Message {
   @Prop(TypeMarkup(), chunter.string.Content)
   @Index(IndexKind.FullText)
   content!: string
+
+  @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
+  attachments?: number
 }
 
 @Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
@@ -44,6 +48,9 @@ export class TComment extends TAttachedDoc implements Comment {
   @Prop(TypeMarkup(), chunter.string.Message)
   @Index(IndexKind.FullText)
   message!: string
+
+  @Prop(Collection(attachment.class.Attachment), attachment.string.Attachments)
+  attachments?: number
 }
 
 @Model(chunter.class.Backlink, chunter.class.Comment)
diff --git a/packages/text-editor/src/components/ReferenceInput.svelte b/packages/text-editor/src/components/ReferenceInput.svelte
index 8faadaf014..edf13f643e 100644
--- a/packages/text-editor/src/components/ReferenceInput.svelte
+++ b/packages/text-editor/src/components/ReferenceInput.svelte
@@ -34,6 +34,7 @@
   const dispatch = createEventDispatcher()
   export let content: string = ''
   export let showSend = true
+  export let withoutTopBorder = false
   const client = getClient()
 
   let textEditor: TextEditor
@@ -53,7 +54,7 @@
     {
       label: textEditorPlugin.string.Attach,
       icon: Attach,
-      action: () => {},
+      action: () => { dispatch('attach') },
       order: 1000
     },
     {
@@ -156,7 +157,7 @@
 </script>
 
 <div class="ref-container">
-  <div class="textInput">
+  <div class="textInput" class:withoutTopBorder>
     <div class="inputMsg">
       <TextEditor bind:content={content} bind:this={textEditor} on:content={
         ev => {
@@ -195,6 +196,11 @@
       border: 1px solid var(--theme-bg-accent-color);
       border-radius: .75rem;
 
+      &.withoutTopBorder {
+        border-top-left-radius: 0;
+        border-top-right-radius: 0;
+      }
+
       .inputMsg {
         display: flex;
         align-self: center;
diff --git a/plugins/attachment-resources/package.json b/plugins/attachment-resources/package.json
index 3e19ea39d3..8c935e23d1 100644
--- a/plugins/attachment-resources/package.json
+++ b/plugins/attachment-resources/package.json
@@ -40,6 +40,7 @@
     "@anticrm/view": "~0.6.0",
     "@anticrm/view-resources": "~0.6.0",
     "@anticrm/panel": "~0.6.0",
+    "@anticrm/text-editor": "~0.6.0",
     "@anticrm/login": "~0.6.1",
     "filesize": "^8.0.3"
   }
diff --git a/plugins/attachment-resources/src/components/AttachmentDocList.svelte b/plugins/attachment-resources/src/components/AttachmentDocList.svelte
new file mode 100644
index 0000000000..9ba1dcb138
--- /dev/null
+++ b/plugins/attachment-resources/src/components/AttachmentDocList.svelte
@@ -0,0 +1,41 @@
+<!--
+// Copyright © 2022 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+-->
+
+<script lang="ts">
+  import { Attachment } from '@anticrm/attachment'
+  import type { Doc } from '@anticrm/core'
+  import { createQuery } from '@anticrm/presentation'
+  import attachment from '../plugin'
+  import AttachmentList from './AttachmentList.svelte'
+
+  export let value: Doc & { attachments?: number }
+
+  const query = createQuery()
+  let attachments: Attachment[] = []
+
+  $: updateQuery(value)
+
+  function updateQuery (value: Doc & { attachments?: number }): void {
+    if (value && value.attachments && value.attachments > 0) {
+      query.query(attachment.class.Attachment, {
+        attachedTo: value._id
+      }, (res) => attachments = res)
+    } else {
+      attachments = []
+    }
+  }
+</script>
+
+<AttachmentList {attachments} />
diff --git a/plugins/attachment-resources/src/components/AttachmentList.svelte b/plugins/attachment-resources/src/components/AttachmentList.svelte
new file mode 100644
index 0000000000..830b710790
--- /dev/null
+++ b/plugins/attachment-resources/src/components/AttachmentList.svelte
@@ -0,0 +1,47 @@
+<!--
+// Copyright © 2022 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+-->
+
+<script lang="ts">
+  import { Attachment } from '@anticrm/attachment'
+  import AttachmentPresenter from './AttachmentPresenter.svelte'
+
+  export let attachments: Attachment[] = []
+</script>
+
+{#if attachments.length}
+  <div class='container'>
+    {#each attachments as attachment}
+      <div class='item'>
+        <AttachmentPresenter value={attachment} />
+      </div>
+    {/each}
+  </div>
+{/if}
+
+<style lang="scss">
+  .container {
+    background-color: var(--theme-bg-accent-color);
+    border: 1px solid var(--theme-bg-accent-color);
+    border-radius: 0.75rem;
+
+    .item {
+      padding: 1rem;
+    }
+
+    .item + .item {
+      border-top: 1px solid var(--theme-bg-accent-color);
+    }
+  }
+</style>
diff --git a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte
new file mode 100644
index 0000000000..d1397507b1
--- /dev/null
+++ b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte
@@ -0,0 +1,156 @@
+
+<!--
+// Copyright © 2022 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+-->
+<script lang="ts">
+  import { createQuery, getClient } from '@anticrm/presentation'
+  import { ReferenceInput } from '@anticrm/text-editor'
+  import { deleteFile, uploadFile } from '../utils'
+  import attachment from '../plugin'
+  import { setPlatformStatus, unknownError } from '@anticrm/platform'
+  import { createEventDispatcher, onDestroy } from 'svelte'
+  import { Class, Doc, Ref, Space } from '@anticrm/core'
+  import { Attachment } from '@anticrm/attachment'
+  import AttachmentPresenter from './AttachmentPresenter.svelte'
+  import { IconClose } from '@anticrm/ui'
+import ActionIcon from '@anticrm/ui/src/components/ActionIcon.svelte';
+
+  export let objectId: Ref<Doc>
+  export let space: Ref<Space>
+  export let _class: Ref<Class<Doc>>
+
+  let inputFile: HTMLInputElement
+  let saved = false
+  const dispatch = createEventDispatcher()
+
+  const client = getClient()
+  const query = createQuery()
+  let attachments: Attachment[] = []
+
+  $: objectId && query.query(attachment.class.Attachment, {
+    attachedTo: objectId
+  }, (res) => attachments = res)
+
+  async function createAttachment (file: File) {
+    try {
+      const uuid = await uploadFile(file, { space, attachedTo: objectId })
+      console.log('uploaded file uuid', uuid)
+      await client.addCollection(attachment.class.Attachment, space, objectId, _class, 'attachments', {
+        name: file.name,
+        file: uuid,
+        type: file.type,
+        size: file.size,
+        lastModified: file.lastModified
+      })
+    } catch (err: any) {
+      setPlatformStatus(unknownError(err))
+    }
+  }
+
+  function fileSelected () {
+    const list = inputFile.files
+    if (list === null || list.length === 0) return
+    for (let index = 0; index < list.length; index++) {
+      const file = list.item(index)
+      if (file !== null) createAttachment(file)
+    }
+  }
+
+  function fileDrop (e: DragEvent) {
+    const list = e.dataTransfer?.files
+    if (list === undefined || list.length === 0) return
+    for (let index = 0; index < list.length; index++) {
+      const file = list.item(index)
+      if (file !== null) createAttachment(file)
+    }
+  }
+
+  async function removeAttachment (attachment: Attachment): Promise<void> {
+    await client.removeCollection(attachment._class, attachment.space, attachment._id, attachment.attachedTo, attachment.attachedToClass, 'attachments')
+    await deleteFile(attachment.file)
+  }
+
+  onDestroy(() => {
+    if (!saved) {
+      attachments.map((attachment) => {
+        removeAttachment(attachment)
+      })
+    }
+  })
+
+  async function onMessage (event: CustomEvent) {
+    saved = true
+    dispatch('message', { message: event.detail, attachments: attachments.length })
+  }
+
+</script>
+
+<input
+  bind:this={inputFile}
+  multiple
+  type="file"
+  name="file"
+  id="file"
+  style="display: none"
+  on:change={fileSelected}
+  />
+<div class="container"
+  on:dragover|preventDefault={() => {}}
+  on:dragleave={() => {}}
+  on:drop|preventDefault|stopPropagation={fileDrop}
+  >
+  {#if attachments.length}
+    <div class='flex-row-center list'>
+      {#each attachments as attachment}
+        <div class='item flex'>
+          <AttachmentPresenter value={attachment} />
+          <div class='remove'>
+            <ActionIcon icon={IconClose} action={() => { removeAttachment(attachment) }} size='small' />
+          </div>
+        </div>
+      {/each}
+    </div>
+  {/if}
+  <ReferenceInput on:message={onMessage} withoutTopBorder={attachments.length > 0} on:attach={() => { inputFile.click() }} />
+</div>
+
+<style lang="scss">
+  .list {
+    padding: 1rem;
+    color: var(--theme-caption-color);
+    overflow-x: auto;
+    background-color: var(--theme-bg-accent-color);
+    border: 1px solid var(--theme-bg-accent-color);
+    border-radius: .75rem;
+    border-bottom-left-radius: 0;
+    border-bottom-right-radius: 0;
+
+    .item + .item {
+      padding-left: 1rem;
+      border-left: 1px solid var(--theme-bg-accent-color);
+    }
+
+    .item {
+      .remove {
+        visibility: hidden;
+      }
+    }
+
+    .item:hover {
+      .remove {
+        visibility: visible;
+      }
+    }
+  }
+</style>
diff --git a/plugins/attachment-resources/src/index.ts b/plugins/attachment-resources/src/index.ts
index 588b925e84..eee6da65e9 100644
--- a/plugins/attachment-resources/src/index.ts
+++ b/plugins/attachment-resources/src/index.ts
@@ -15,13 +15,16 @@
 
 import AttachmentsPresenter from './components/AttachmentsPresenter.svelte'
 import AttachmentPresenter from './components/AttachmentPresenter.svelte'
+import AttachmentDocList from './components/AttachmentDocList.svelte'
+import AttachmentList from './components/AttachmentList.svelte'
+import AttachmentRefInput from './components/AttachmentRefInput.svelte'
 import TxAttachmentCreate from './components/activity/TxAttachmentCreate.svelte'
 import Attachments from './components/Attachments.svelte'
 import Photos from './components/Photos.svelte'
 import { Resources } from '@anticrm/platform'
 import { uploadFile, deleteFile } from './utils'
 
-export { Attachments, AttachmentsPresenter }
+export { Attachments, AttachmentsPresenter, AttachmentRefInput, AttachmentList, AttachmentDocList }
 
 export default async (): Promise<Resources> => ({
   component: {
diff --git a/plugins/chunter-resources/package.json b/plugins/chunter-resources/package.json
index 292816ff4d..b6b741b3ce 100644
--- a/plugins/chunter-resources/package.json
+++ b/plugins/chunter-resources/package.json
@@ -41,6 +41,8 @@
     "@anticrm/text-editor": "~0.6.0",
     "@anticrm/contact": "~0.6.2",
     "@anticrm/contact-resources": "~0.6.0",
+    "@anticrm/attachment": "~0.6.0",
+    "@anticrm/attachment-resources": "~0.6.0",
     "@anticrm/view-resources": "~0.6.0",
     "@anticrm/view": "~0.6.0",
     "@anticrm/workbench": "~0.6.1"
diff --git a/plugins/chunter-resources/src/components/Channel.svelte b/plugins/chunter-resources/src/components/Channel.svelte
index 840b375e66..3e028c5cb7 100644
--- a/plugins/chunter-resources/src/components/Channel.svelte
+++ b/plugins/chunter-resources/src/components/Channel.svelte
@@ -17,6 +17,7 @@
   import type { Ref, Space } from '@anticrm/core'
   import { createQuery } from '@anticrm/presentation'
   import type { Message } from '@anticrm/chunter'
+  import attachment from '@anticrm/attachment'
   import chunter from '../plugin'
 
   import { default as MessageComponent } from './Message.svelte'
@@ -26,7 +27,7 @@
   let messages: Message[] | undefined
   const query = createQuery()
 
-  $: query.query(chunter.class.Message, { space }, result => { messages = result })
+  $: query.query(chunter.class.Message, { space }, result => { messages = result }, { lookup: { _id: { attachments: attachment.class.Attachment } }})
 </script>
 
 <div class="flex-col container">
diff --git a/plugins/chunter-resources/src/components/ChannelView.svelte b/plugins/chunter-resources/src/components/ChannelView.svelte
index 7daf02d575..9b62250b6c 100644
--- a/plugins/chunter-resources/src/components/ChannelView.svelte
+++ b/plugins/chunter-resources/src/components/ChannelView.svelte
@@ -14,25 +14,30 @@
 -->
 
 <script lang="ts">
-  import type { Ref, Space } from '@anticrm/core'
+  import { generateId, Ref, Space } from '@anticrm/core'
   import chunter from '../plugin'
   import { getClient } from '@anticrm/presentation'
 
   import Channel from './Channel.svelte'
-  import { ReferenceInput } from '@anticrm/text-editor'
+  import { AttachmentRefInput } from '@anticrm/attachment-resources'
   import { createBacklinks } from '../backlinks'
 
   export let space: Ref<Space>
 
   const client = getClient()
+  const _class = chunter.class.Message
+  let _id = generateId()
 
   async function onMessage (event: CustomEvent) {
-    const msgRef = await client.createDoc(chunter.class.Message, space, {
-      content: event.detail
-    })
+    const { message, attachments } = event.detail
+    await client.createDoc(_class, space, {
+      content: message,
+      attachments
+    }, _id)
   
     // Create an backlink to document
-    await createBacklinks(client, space, chunter.class.Channel, msgRef, event.detail)
+    await createBacklinks(client, space, chunter.class.Channel, _id, message)
+    _id = generateId()
   }
 </script>
 
@@ -40,7 +45,7 @@
   <Channel {space} />
 </div>
 <div class="reference">
-  <ReferenceInput on:message={onMessage}/>
+  <AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage}/>
 </div>
 
 <style lang="scss">
diff --git a/plugins/chunter-resources/src/components/CommentInput.svelte b/plugins/chunter-resources/src/components/CommentInput.svelte
index 7c784bf264..1dc479ae24 100644
--- a/plugins/chunter-resources/src/components/CommentInput.svelte
+++ b/plugins/chunter-resources/src/components/CommentInput.svelte
@@ -16,21 +16,25 @@
 -->
 <script lang="ts">
   import { Comment } from '@anticrm/chunter'
-  import { Doc } from '@anticrm/core'
+  import { Doc, generateId, Ref } from '@anticrm/core'
   import { getClient } from '@anticrm/presentation'
-  import { ReferenceInput } from '@anticrm/text-editor'
+  import { AttachmentRefInput } from '@anticrm/attachment-resources'
   import { createBacklinks } from '../backlinks'
   import chunter from '../plugin'
 
   const client = getClient()
   export let object: Doc
+  const _class = chunter.class.Comment
+  let _id: Ref<Comment> = generateId()
   
   async function onMessage (event: CustomEvent) {
-    const commentId = await client.addCollection<Doc, Comment>(chunter.class.Comment, object.space, object._id, object._class, 'comments', { message: event.detail })
+    const { message, attachments } = event.detail
+    await client.addCollection<Doc, Comment>(_class, object.space, object._id, object._class, 'comments', { message, attachments }, _id)
 
     // Create an backlink to document
-    await createBacklinks(client, object._id, object._class, commentId, event.detail)
+    await createBacklinks(client, object._id, object._class, _id, message)
+    _id = generateId()
   }
 
 </script>
-<ReferenceInput on:message={onMessage} />
\ No newline at end of file
+<AttachmentRefInput {_class} space={object.space} objectId={_id} on:message={onMessage}/>
\ No newline at end of file
diff --git a/plugins/chunter-resources/src/components/Message.svelte b/plugins/chunter-resources/src/components/Message.svelte
index 7454a9083d..d68e0f0bf1 100644
--- a/plugins/chunter-resources/src/components/Message.svelte
+++ b/plugins/chunter-resources/src/components/Message.svelte
@@ -26,14 +26,19 @@
   import { MessageViewer } from '@anticrm/presentation'
   import { getTime, getUser } from '../utils'
   import { formatName } from '@anticrm/contact'
+  import { AttachmentList } from '@anticrm/attachment-resources'
+  import { WithLookup } from '@anticrm/core'
+  import { Attachment } from '@anticrm/attachment'
 
-  export let message: Message
+  export let message: WithLookup<Message>
 
   let reactions: boolean = false
   let replies: boolean = false
   let thread: boolean = false
 
   const client = getClient()
+
+  $: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
 </script>
 
 <div class="container">
@@ -46,6 +51,7 @@
     <span>{getTime(message.modifiedOn)}</span>
     </div>
     <div class="text"><MessageViewer message={message.content}/></div>
+    <div class="attachments"><AttachmentList {attachments} /></div>
     {#if (reactions || replies) && !thread}
       <div class="footer">
         <div>{#if reactions}<Reactions/>{/if}</div>
@@ -96,6 +102,9 @@
       .text {
         line-height: 150%;
       }
+      .attachments {
+        margin-top: 1rem;
+      }
       .footer {
         display: flex;
         justify-content: space-between;
diff --git a/plugins/chunter-resources/src/components/activity/TxCommentCreate.svelte b/plugins/chunter-resources/src/components/activity/TxCommentCreate.svelte
index 31f3479537..d8f54b3225 100644
--- a/plugins/chunter-resources/src/components/activity/TxCommentCreate.svelte
+++ b/plugins/chunter-resources/src/components/activity/TxCommentCreate.svelte
@@ -17,6 +17,7 @@
   import type { Comment } from '@anticrm/chunter'
   import type { TxCreateDoc } from '@anticrm/core'
   import { getClient, MessageViewer } from '@anticrm/presentation'
+  import { AttachmentDocList } from '@anticrm/attachment-resources'
   import { ReferenceInput } from '@anticrm/text-editor'
   import { Button } from '@anticrm/ui'
   import { createEventDispatcher } from 'svelte'
@@ -55,6 +56,7 @@
     </div>
   {:else}
     <MessageViewer message={value.message}/>
+    <AttachmentDocList {value} />
   {/if}
 </div>
 
diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts
index 685f529ec5..7e53545452 100644
--- a/plugins/chunter/src/index.ts
+++ b/plugins/chunter/src/index.ts
@@ -28,6 +28,7 @@ export interface Channel extends Space {}
  */
 export interface Message extends Doc {
   content: string
+  attachments?: number
 }
 
 /**
@@ -35,6 +36,7 @@ export interface Message extends Doc {
  */
 export interface Comment extends AttachedDoc {
   message: string
+  attachments?: number
 }
 
 /**