From 6739ce302df928dbe309442cfe20c0b1c17c40a4 Mon Sep 17 00:00:00 2001
From: budaeva <47611627+budaeva@users.noreply.github.com>
Date: Tue, 31 May 2022 20:01:24 +0700
Subject: [PATCH] Messages search (#1935)

Signed-off-by: budaeva <irina.budaeva@xored.com>
---
 models/chunter/src/index.ts                   |  12 ++
 models/chunter/src/plugin.ts                  |  13 +-
 plugins/chunter-assets/lang/en.json           |   5 +-
 plugins/chunter-assets/lang/ru.json           |   5 +-
 .../src/components/CreateDirectMessage.svelte |   1 -
 .../src/components/DmHeader.svelte            |  18 ++-
 .../src/components/Header.svelte              |  18 ++-
 .../src/components/MessagesBrowser.svelte     | 128 ++++++++++++++++++
 .../src/components/SavedMessages.svelte       |  30 ++--
 plugins/chunter-resources/src/index.ts        |  14 +-
 plugins/chunter-resources/src/plugin.ts       |   3 +-
 plugins/chunter-resources/src/utils.ts        |  17 ++-
 plugins/view-resources/src/index.ts           |   2 +
 13 files changed, 233 insertions(+), 33 deletions(-)
 create mode 100644 plugins/chunter-resources/src/components/MessagesBrowser.svelte

diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts
index 605de85b48..67fcfb919e 100644
--- a/models/chunter/src/index.ts
+++ b/models/chunter/src/index.ts
@@ -93,6 +93,7 @@ export class TChunterMessage extends TAttachedDoc implements ChunterMessage {
 }
 
 @Model(chunter.class.ThreadMessage, chunter.class.ChunterMessage)
+@UX(chunter.string.ThreadMessage)
 export class TThreadMessage extends TChunterMessage implements ThreadMessage {
   declare attachedTo: Ref<Message>
 
@@ -100,6 +101,7 @@ export class TThreadMessage extends TChunterMessage implements ThreadMessage {
 }
 
 @Model(chunter.class.Message, chunter.class.ChunterMessage)
+@UX(chunter.string.Message)
 export class TMessage extends TChunterMessage implements Message {
   declare attachedTo: Ref<Space>
 
@@ -347,6 +349,12 @@ export function createModel (builder: Builder): void {
             componentProps: {
               requestedSpaceClasses: [chunter.class.Channel, chunter.class.DirectMessage]
             }
+          },
+          {
+            id: 'messagesBrowser',
+            label: chunter.string.MessagesBrowser,
+            component: chunter.component.MessagesBrowser,
+            visibleIf: chunter.function.MessageBrowserVisible
           }
         ],
         spaces: [
@@ -437,6 +445,10 @@ export function createModel (builder: Builder): void {
     },
     chunter.ids.TxBacklinkRemove
   )
+
+  builder.mixin(chunter.class.ChunterMessage, core.class.Class, view.mixin.ClassFilters, {
+    filters: ['space', 'modifiedOn', 'createBy', '_class']
+  })
 }
 
 export { chunterOperation } from './migration'
diff --git a/models/chunter/src/plugin.ts b/models/chunter/src/plugin.ts
index 3eaf6ec341..1ec84e2a10 100644
--- a/models/chunter/src/plugin.ts
+++ b/models/chunter/src/plugin.ts
@@ -16,8 +16,8 @@
 import type { TxViewlet } from '@anticrm/activity'
 import { Channel, chunterId } from '@anticrm/chunter'
 import chunter from '@anticrm/chunter-resources/src/plugin'
-import type { Ref } from '@anticrm/core'
-import type { IntlString } from '@anticrm/platform'
+import type { Ref, Space } from '@anticrm/core'
+import type { IntlString, Resource } from '@anticrm/platform'
 import { mergeIds } from '@anticrm/platform'
 import type { AnyComponent } from '@anticrm/ui'
 import type { Action, ActionCategory, ViewAction, ViewletDescriptor } from '@anticrm/view'
@@ -29,7 +29,8 @@ export default mergeIds(chunterId, chunter, {
     DmPresenter: '' as AnyComponent,
     Threads: '' as AnyComponent,
     ThreadView: '' as AnyComponent,
-    SavedMessages: '' as AnyComponent
+    SavedMessages: '' as AnyComponent,
+    MessagesBrowser: '' as AnyComponent
   },
   action: {
     MarkCommentUnread: '' as Ref<Action>,
@@ -63,7 +64,8 @@ export default mergeIds(chunterId, chunter, {
     MarkUnread: '' as IntlString,
     LastMessage: '' as IntlString,
     PinnedMessages: '' as IntlString,
-    SavedMessages: '' as IntlString
+    SavedMessages: '' as IntlString,
+    ThreadMessage: '' as IntlString
   },
   viewlet: {
     Chat: '' as Ref<ViewletDescriptor>
@@ -82,5 +84,8 @@ export default mergeIds(chunterId, chunter, {
   space: {
     General: '' as Ref<Channel>,
     Random: '' as Ref<Channel>
+  },
+  function: {
+    MessageBrowserVisible: '' as Resource<(spaces: Space[]) => boolean>
   }
 })
diff --git a/plugins/chunter-assets/lang/en.json b/plugins/chunter-assets/lang/en.json
index 967517f736..72459fe2a7 100644
--- a/plugins/chunter-assets/lang/en.json
+++ b/plugins/chunter-assets/lang/en.json
@@ -59,6 +59,9 @@
     "ChannelBrowser": "Channel browser",
     "SavedItems": "Saved items",
     "AddMembersHeader": "Add members to {channel}:",
-    "ConvertToPrivate": "Convert to private channel"
+    "ConvertToPrivate": "Convert to private channel",
+    "MessagesBrowser": "Messages browser",
+    "CreateBy": "Create by",
+    "ThreadMessage": "Thread message"
   }
 }
\ No newline at end of file
diff --git a/plugins/chunter-assets/lang/ru.json b/plugins/chunter-assets/lang/ru.json
index f710feffe1..af3fb5ba0b 100644
--- a/plugins/chunter-assets/lang/ru.json
+++ b/plugins/chunter-assets/lang/ru.json
@@ -58,6 +58,9 @@
     "ChannelBrowser": "Браузер каналов",
     "SavedItems": "Сохраненные объекты",
     "AddMembersHeader": "Добавить пользователей в {channel}:",
-    "ConvertToPrivate": "Конвертировать в закрытый канал"
+    "ConvertToPrivate": "Конвертировать в закрытый канал",
+    "MessagesBrowser": "Браузер сообщений",
+    "CreateBy": "Создано пользователем",
+    "ThreadMessage": "Сообщение в обсуждении"
   }
 }
\ No newline at end of file
diff --git a/plugins/chunter-resources/src/components/CreateDirectMessage.svelte b/plugins/chunter-resources/src/components/CreateDirectMessage.svelte
index d31283e432..56911203c5 100644
--- a/plugins/chunter-resources/src/components/CreateDirectMessage.svelte
+++ b/plugins/chunter-resources/src/components/CreateDirectMessage.svelte
@@ -43,7 +43,6 @@
           mode: 'space',
           space: dm._id
         })
-
         return
       }
     }
diff --git a/plugins/chunter-resources/src/components/DmHeader.svelte b/plugins/chunter-resources/src/components/DmHeader.svelte
index 0762627328..5ebec0d0e0 100644
--- a/plugins/chunter-resources/src/components/DmHeader.svelte
+++ b/plugins/chunter-resources/src/components/DmHeader.svelte
@@ -18,12 +18,16 @@
   import { getCurrentAccount } from '@anticrm/core'
   import { createQuery, getClient, CombineAvatars } from '@anticrm/presentation'
   import contact, { EmployeeAccount } from '@anticrm/contact'
-  import { showPanel } from '@anticrm/ui'
+  import { getCurrentLocation, navigate, SearchEdit, showPanel } from '@anticrm/ui'
   import chunter from '../plugin'
   import { getDmName } from '../utils'
+  import { userSearch } from '../index'
 
   export let spaceId: Ref<DirectMessage> | undefined
 
+  let userSearch_: string = ''
+  userSearch.subscribe((v) => (userSearch_ = v))
+
   const client = getClient()
   const query = createQuery()
   const myAccId = getCurrentAccount()._id
@@ -62,6 +66,18 @@
       {/await}
     {/await}
   {/if}
+  <SearchEdit
+    value={userSearch_}
+    on:change={(ev) => {
+      userSearch.set(ev.detail)
+
+      if (ev.detail !== '') {
+        const loc = getCurrentLocation()
+        loc.path[2] = 'messagesBrowser'
+        navigate(loc)
+      }
+    }}
+  />
 </div>
 
 <style lang="scss">
diff --git a/plugins/chunter-resources/src/components/Header.svelte b/plugins/chunter-resources/src/components/Header.svelte
index e5b7c990fc..b708fe9654 100644
--- a/plugins/chunter-resources/src/components/Header.svelte
+++ b/plugins/chunter-resources/src/components/Header.svelte
@@ -14,11 +14,15 @@
 -->
 <script lang="ts">
   import type { Asset } from '@anticrm/platform'
-  import { AnySvelteComponent, Icon } from '@anticrm/ui'
+  import { AnySvelteComponent, getCurrentLocation, Icon, navigate, SearchEdit } from '@anticrm/ui'
+  import { userSearch } from '../index'
 
   export let icon: Asset | AnySvelteComponent | undefined
   export let label: string
   export let description: string | undefined
+
+  let userSearch_: string
+  userSearch.subscribe((v) => (userSearch_ = v))
 </script>
 
 <div class="ac-header__wrap-description">
@@ -28,6 +32,18 @@
   </div>
   {#if description}<span class="ac-header__description">{description}</span>{/if}
 </div>
+<SearchEdit
+  value={userSearch_}
+  on:change={(ev) => {
+    userSearch.set(ev.detail)
+
+    if (ev.detail !== '') {
+      const loc = getCurrentLocation()
+      loc.path[2] = 'messagesBrowser'
+      navigate(loc)
+    }
+  }}
+/>
 
 <style lang="scss">
   .ac-header__wrap-title:hover {
diff --git a/plugins/chunter-resources/src/components/MessagesBrowser.svelte b/plugins/chunter-resources/src/components/MessagesBrowser.svelte
new file mode 100644
index 0000000000..30a3a77d71
--- /dev/null
+++ b/plugins/chunter-resources/src/components/MessagesBrowser.svelte
@@ -0,0 +1,128 @@
+<script lang="ts">
+  import attachment, { Attachment } from '@anticrm/attachment'
+  import chunter, { ChunterMessage } from '@anticrm/chunter'
+  import contact, { Employee } from '@anticrm/contact'
+  import core, { DocumentQuery, Ref, SortingOrder } from '@anticrm/core'
+  import { createQuery, getClient } from '@anticrm/presentation'
+  import { Label, Scroller, SearchEdit } from '@anticrm/ui'
+  import type { Filter } from '@anticrm/view'
+  import { FilterBar, FilterButton } from '@anticrm/view-resources'
+  import MessageComponent from './Message.svelte'
+  import { userSearch } from '../index'
+  import plugin from '../plugin'
+  import { openMessageFromSpecial } from '../utils'
+
+  let userSearch_: string = ''
+  userSearch.subscribe((v) => (userSearch_ = v))
+
+  let searchQuery: DocumentQuery<ChunterMessage> = { $search: userSearch_ }
+
+  let filters: Filter[] = []
+
+  function updateSearchQuery (search: string): void {
+    searchQuery = { $search: search }
+  }
+
+  $: updateSearchQuery(userSearch_)
+
+  const client = getClient()
+  const _class = chunter.class.ChunterMessage
+  let messages: ChunterMessage[] = []
+
+  let resultQuery: DocumentQuery<ChunterMessage> = { ...searchQuery }
+
+  async function updateMessages (resultQuery: DocumentQuery<ChunterMessage>) {
+    messages = await client.findAll(
+      _class,
+      {
+        ...resultQuery
+      },
+      {
+        sort: { createOn: SortingOrder.Descending },
+        limit: 100,
+        lookup: {
+          _id: { attachments: attachment.class.Attachment },
+          createBy: core.class.Account
+        }
+      }
+    )
+  }
+
+  $: updateMessages(resultQuery)
+
+  let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>()
+  const employeeQuery = createQuery()
+
+  employeeQuery.query(
+    contact.class.Employee,
+    {},
+    (res) =>
+      (employees = new Map(
+        res.map((r) => {
+          return [r._id, r]
+        })
+      )),
+    {
+      lookup: { _id: { statuses: contact.class.Status } }
+    }
+  )
+
+  const pinnedQuery = createQuery()
+  const pinnedIds: Ref<ChunterMessage>[] = []
+
+  pinnedQuery.query(
+    chunter.class.Channel,
+    {},
+    (res) => {
+      res.forEach((ch) => {
+        if (ch.pinned) {
+          pinnedIds.push(...ch.pinned)
+        }
+      })
+    },
+    {}
+  )
+  let savedMessagesIds: Ref<ChunterMessage>[] = []
+  let savedAttachmentsIds: Ref<Attachment>[] = []
+
+  const savedMessagesQuery = createQuery()
+  const savedAttachmentsQuery = createQuery()
+
+  savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
+    savedMessagesIds = res.map((r) => r.attachedTo)
+  })
+
+  savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
+    savedAttachmentsIds = res.map((r) => r.attachedTo)
+  })
+</script>
+
+<div class="ac-header full divide">
+  <div class="ac-header__wrap-title">
+    <span class="ac-header__title"><Label label={plugin.string.MessagesBrowser} /></span>
+  </div>
+  <div class="ml-4"><FilterButton {_class} bind:filters /></div>
+  <SearchEdit
+    value={userSearch_}
+    on:change={(ev) => {
+      userSearch.set(ev.detail)
+      updateSearchQuery(userSearch_)
+      updateMessages(resultQuery)
+    }}
+  />
+</div>
+<FilterBar {_class} query={searchQuery} bind:filters on:change={(e) => (resultQuery = e.detail)} />
+<Scroller>
+  {#each messages as message}
+    <div on:click={() => openMessageFromSpecial(message)}>
+      <MessageComponent
+        {message}
+        {employees}
+        on:openThread
+        isPinned={pinnedIds.includes(message._id)}
+        isSaved={savedMessagesIds.includes(message._id)}
+        {savedAttachmentsIds}
+      />
+    </div>
+  {/each}
+</Scroller>
diff --git a/plugins/chunter-resources/src/components/SavedMessages.svelte b/plugins/chunter-resources/src/components/SavedMessages.svelte
index 043acb9956..63c5d68969 100644
--- a/plugins/chunter-resources/src/components/SavedMessages.svelte
+++ b/plugins/chunter-resources/src/components/SavedMessages.svelte
@@ -4,12 +4,12 @@
   import { ChunterMessage } from '@anticrm/chunter'
   import core, { Ref, WithLookup } from '@anticrm/core'
   import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
-  import { getCurrentLocation, Label, navigate, Scroller } from '@anticrm/ui'
+  import { Label, Scroller } from '@anticrm/ui'
   import AttachmentPreview from '@anticrm/attachment-resources/src/components/AttachmentPreview.svelte'
   import Bookmark from './icons/Bookmark.svelte'
   import Message from './Message.svelte'
   import chunter from '../plugin'
-  import { getTime } from '../utils'
+  import { getTime, openMessageFromSpecial } from '../utils'
 
   const client = getClient()
   let savedMessagesIds: Ref<ChunterMessage>[] = []
@@ -83,29 +83,19 @@
     chunter.class.Channel,
     {},
     (res) => {
-      res.forEach((ch) => pinnedIds.concat(ch?.pinned ?? []))
+      res.forEach((ch) => {
+        if (ch.pinned) {
+          pinnedIds.push(...ch.pinned)
+        }
+      })
     },
-    { limit: 1 }
+    {}
   )
 
-  function openMessage (message: ChunterMessage) {
-    const loc = getCurrentLocation()
-
-    if (message.attachedToClass === chunter.class.ChunterSpace) {
-      loc.path.length = 3
-      loc.path[2] = message.attachedTo
-    } else if (message.attachedToClass === chunter.class.Message) {
-      loc.path.length = 4
-      loc.path[2] = message.space
-      loc.path[3] = message.attachedTo
-    }
-    navigate(loc)
-  }
-
   async function openAttachment (att: Attachment) {
     const messageId: Ref<ChunterMessage> = att.attachedTo as Ref<ChunterMessage>
     await client.findOne(chunter.class.ChunterMessage, { _id: messageId }).then((res) => {
-      if (res !== undefined) openMessage(res)
+      if (res !== undefined) openMessageFromSpecial(res)
     })
   }
 
@@ -125,7 +115,7 @@
 <Scroller>
   {#if savedMessages.length > 0 || savedAttachments.length > 0}
     {#each savedMessages as message}
-      <div on:click={() => openMessage(message)}>
+      <div on:click={() => openMessageFromSpecial(message)}>
         <Message
           {message}
           {employees}
diff --git a/plugins/chunter-resources/src/index.ts b/plugins/chunter-resources/src/index.ts
index 5851773855..6f11faec41 100644
--- a/plugins/chunter-resources/src/index.ts
+++ b/plugins/chunter-resources/src/index.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 //
 
-import core from '@anticrm/core'
+import core, { Space } from '@anticrm/core'
 import chunter, { ChunterSpace, Channel, ChunterMessage, Message, ThreadMessage, DirectMessage } from '@anticrm/chunter'
 import { NotificationClientImpl } from '@anticrm/notification-resources'
 import { Resources } from '@anticrm/platform'
@@ -34,12 +34,14 @@ import CommentsPresenter from './components/CommentsPresenter.svelte'
 import CreateChannel from './components/CreateChannel.svelte'
 import CreateDirectMessage from './components/CreateDirectMessage.svelte'
 import EditChannel from './components/EditChannel.svelte'
+import MessagesBrowser from './components/MessagesBrowser.svelte'
 import ThreadView from './components/ThreadView.svelte'
 import Threads from './components/Threads.svelte'
 import SavedMessages from './components/SavedMessages.svelte'
 import ConvertDmToPrivateChannelModal from './components/ConvertDmToPrivateChannel.svelte'
 
 import { getDmName } from './utils'
+import { writable } from 'svelte/store'
 
 export { default as Header } from './components/Header.svelte'
 export { classIcon } from './utils'
@@ -160,6 +162,12 @@ export async function DeleteMessageFromSaved (message: ChunterMessage): Promise<
   }
 }
 
+export const userSearch = writable('')
+
+export function messageBrowserVisible (spaces: Space[]): boolean {
+  return false
+}
+
 export default async (): Promise<Resources> => ({
   component: {
     CommentInput,
@@ -173,12 +181,14 @@ export default async (): Promise<Resources> => ({
     ChannelPresenter,
     DmPresenter,
     EditChannel,
+    MessagesBrowser,
     Threads,
     ThreadView,
     SavedMessages
   },
   function: {
-    GetDmName: getDmName
+    GetDmName: getDmName,
+    MessageBrowserVisible: messageBrowserVisible
   },
   activity: {
     TxCommentCreate,
diff --git a/plugins/chunter-resources/src/plugin.ts b/plugins/chunter-resources/src/plugin.ts
index f3569d72ad..c08b390f4c 100644
--- a/plugins/chunter-resources/src/plugin.ts
+++ b/plugins/chunter-resources/src/plugin.ts
@@ -81,6 +81,7 @@ export default mergeIds(chunterId, chunter, {
     LeaveChannel: '' as IntlString,
     ChannelBrowser: '' as IntlString,
     SavedItems: '' as IntlString,
-    AddMembersHeader: '' as IntlString
+    AddMembersHeader: '' as IntlString,
+    MessagesBrowser: '' as IntlString
   }
 })
diff --git a/plugins/chunter-resources/src/utils.ts b/plugins/chunter-resources/src/utils.ts
index 46e651c454..41e306d3ff 100644
--- a/plugins/chunter-resources/src/utils.ts
+++ b/plugins/chunter-resources/src/utils.ts
@@ -1,7 +1,8 @@
+import { ChunterMessage } from '@anticrm/chunter'
 import contact, { EmployeeAccount, formatName } from '@anticrm/contact'
 import { Account, Class, Client, Obj, Ref, Space, getCurrentAccount, Timestamp } from '@anticrm/core'
 import { Asset } from '@anticrm/platform'
-import { getCurrentLocation, locationToUrl } from '@anticrm/ui'
+import { getCurrentLocation, locationToUrl, navigate } from '@anticrm/ui'
 
 import chunter from './plugin'
 
@@ -69,3 +70,17 @@ export function getDay (time: Timestamp): Timestamp {
   date.setHours(0, 0, 0, 0)
   return date.getTime()
 }
+
+export function openMessageFromSpecial (message: ChunterMessage): void {
+  const loc = getCurrentLocation()
+
+  if (message.attachedToClass === chunter.class.ChunterSpace) {
+    loc.path.length = 3
+    loc.path[2] = message.attachedTo
+  } else if (message.attachedToClass === chunter.class.Message) {
+    loc.path.length = 4
+    loc.path[2] = message.space
+    loc.path[3] = message.attachedTo
+  }
+  navigate(loc)
+}
diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts
index 921d3f7e29..0352c81fc0 100644
--- a/plugins/view-resources/src/index.ts
+++ b/plugins/view-resources/src/index.ts
@@ -45,6 +45,7 @@ import ValueFilter from './components/filter/ValueFilter.svelte'
 import ObjectFilter from './components/filter/ObjectFilter.svelte'
 import TimestampFilter from './components/filter/TimestampFilter.svelte'
 import ClassPresenter from './components/ClassPresenter.svelte'
+import FilterBar from './components/filter/FilterBar.svelte'
 import EditBoxPopup from './components/EditBoxPopup.svelte'
 import BooleanTruePresenter from './components/BooleanTruePresenter.svelte'
 import EnumEditor from './components/EnumEditor.svelte'
@@ -75,6 +76,7 @@ export {
   SpacePresenter,
   UpDownNavigator,
   ViewletSetting,
+  FilterBar,
   ClassAttributeBar
 }