Chunter: saved attachments (#1515)

Signed-off-by: budaeva <irina.budaeva@xored.com>
This commit is contained in:
budaeva 2022-04-25 20:44:43 +07:00 committed by GitHub
parent 25bc560b8b
commit e5c64849ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 344 additions and 93 deletions

View File

@ -33,6 +33,7 @@
"@anticrm/platform": "~0.6.5", "@anticrm/platform": "~0.6.5",
"@anticrm/model-core": "~0.6.0", "@anticrm/model-core": "~0.6.0",
"@anticrm/model-view": "~0.6.0", "@anticrm/model-view": "~0.6.0",
"@anticrm/activity": "~0.6.0" "@anticrm/activity": "~0.6.0",
"@anticrm/model-preference": "~0.6.0"
} }
} }

View File

@ -14,11 +14,12 @@
// //
import activity from '@anticrm/activity' import activity from '@anticrm/activity'
import type { Attachment, Photo } from '@anticrm/attachment' import type { Attachment, Photo, SavedAttachments } from '@anticrm/attachment'
import { Domain, IndexKind } from '@anticrm/core' import { Domain, IndexKind, Ref } from '@anticrm/core'
import { Builder, Index, Model, Prop, TypeString, TypeTimestamp, UX } from '@anticrm/model' import { Builder, Index, Model, Prop, TypeRef, TypeString, TypeTimestamp, UX } from '@anticrm/model'
import core, { TAttachedDoc } from '@anticrm/model-core' import core, { TAttachedDoc } from '@anticrm/model-core'
import view from '@anticrm/model-view' import view from '@anticrm/model-view'
import preference, { TPreference } from '@anticrm/model-preference'
import attachment from './plugin' import attachment from './plugin'
export { attachmentOperation } from './migration' export { attachmentOperation } from './migration'
@ -50,8 +51,14 @@ export class TAttachment extends TAttachedDoc implements Attachment {
@UX(attachment.string.Photo) @UX(attachment.string.Photo)
export class TPhoto extends TAttachment implements Photo {} export class TPhoto extends TAttachment implements Photo {}
@Model(attachment.class.SavedAttachments, preference.class.Preference)
export class TSavedAttachments extends TPreference implements SavedAttachments {
@Prop(TypeRef(attachment.class.Attachment), attachment.string.SavedAttachments)
attachedTo!: Ref<Attachment>
}
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
builder.createModel(TAttachment, TPhoto) builder.createModel(TAttachment, TPhoto, TSavedAttachments)
builder.mixin(attachment.class.Attachment, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(attachment.class.Attachment, core.class.Class, view.mixin.AttributePresenter, {
presenter: attachment.component.AttachmentPresenter presenter: attachment.component.AttachmentPresenter

View File

@ -33,7 +33,8 @@ export default mergeIds(attachmentId, attachment, {
Size: '' as IntlString, Size: '' as IntlString,
Type: '' as IntlString, Type: '' as IntlString,
Photo: '' as IntlString, Photo: '' as IntlString,
Date: '' as IntlString Date: '' as IntlString,
SavedAttachments: '' as IntlString
}, },
ids: { ids: {
TxAttachmentCreate: '' as Ref<TxViewlet> TxAttachmentCreate: '' as Ref<TxViewlet>

View File

@ -16,9 +16,9 @@
import activity from '@anticrm/activity' import activity from '@anticrm/activity'
import type { import type {
Backlink, Backlink,
ChunterSpace,
Channel, Channel,
ChunterMessage, ChunterMessage,
ChunterSpace,
Comment, Comment,
Message, Message,
SavedMessages, SavedMessages,

View File

@ -36,6 +36,8 @@
"FileBrowserTypeFilterImages": "Images", "FileBrowserTypeFilterImages": "Images",
"FileBrowserTypeFilterAudio": "Audio", "FileBrowserTypeFilterAudio": "Audio",
"FileBrowserTypeFilterVideos": "Videos", "FileBrowserTypeFilterVideos": "Videos",
"FileBrowserTypeFilterPDFs": "PDFs" "FileBrowserTypeFilterPDFs": "PDFs",
"AddAttachmentToSaved": "Add attachment to saved",
"RemoveAttachmentFromSaved": "Remove attachment from saved"
} }
} }

View File

@ -36,6 +36,8 @@
"FileBrowserTypeFilterImages": "Изображения", "FileBrowserTypeFilterImages": "Изображения",
"FileBrowserTypeFilterAudio": "Звук", "FileBrowserTypeFilterAudio": "Звук",
"FileBrowserTypeFilterVideos": "Видео", "FileBrowserTypeFilterVideos": "Видео",
"FileBrowserTypeFilterPDFs": "PDF-файлы" "FileBrowserTypeFilterPDFs": "PDF-файлы",
"AddAttachmentToSaved": "Добавить вложение в сохраненные",
"RemoveAttachmentFromSaved": "Удалить вложение из сохраненных"
} }
} }

View File

@ -43,6 +43,7 @@
"@anticrm/panel": "~0.6.0", "@anticrm/panel": "~0.6.0",
"@anticrm/text-editor": "~0.6.0", "@anticrm/text-editor": "~0.6.0",
"@anticrm/login": "~0.6.1", "@anticrm/login": "~0.6.1",
"filesize": "^8.0.3" "filesize": "^8.0.3",
"@anticrm/preference": "~0.6.0"
} }
} }

View File

@ -14,16 +14,18 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Attachment } from '@anticrm/attachment' import { Attachment } from '@anticrm/attachment'
import { Ref } from '@anticrm/core'
import AttachmentPreview from './AttachmentPreview.svelte' import AttachmentPreview from './AttachmentPreview.svelte'
export let attachments: Attachment[] = [] export let attachments: Attachment[] = []
export let savedAttachmentsIds: Ref<Attachment>[] = []
</script> </script>
{#if attachments.length} {#if attachments.length}
<div class="container"> <div class="container">
{#each attachments as attachment} {#each attachments as attachment}
<div class="item"> <div class="item">
<AttachmentPreview value={attachment} /> <AttachmentPreview value={attachment} isSaved={savedAttachmentsIds?.includes(attachment._id) ?? false} />
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -13,43 +13,116 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Attachment } from '@anticrm/attachment' import type { Attachment } from '@anticrm/attachment'
import { getResource } from '@anticrm/platform'
import { getFileUrl, PDFViewer } from '@anticrm/presentation' import { getFileUrl, PDFViewer } from '@anticrm/presentation'
import { showPopup, closeTooltip, ActionIcon, IconMoreH, Menu } from '@anticrm/ui'
import { Action } from '@anticrm/view'
import { getType } from '../utils' import { getType } from '../utils'
import { showPopup, closeTooltip } from '@anticrm/ui'
import AttachmentPresenter from './AttachmentPresenter.svelte' import AttachmentPresenter from './AttachmentPresenter.svelte'
import AudioPlayer from './AudioPlayer.svelte' import AudioPlayer from './AudioPlayer.svelte'
import attachment from '../plugin'
export let value: Attachment export let value: Attachment
export let isSaved: boolean = false
$: type = getType(value.type) $: type = getType(value.type)
$: saveAttachmentAction = isSaved
? ({
label: attachment.string.RemoveAttachmentFromSaved,
action: attachment.actionImpl.DeleteAttachmentFromSaved
} as Action)
: ({
label: attachment.string.AddAttachmentToSaved,
action: attachment.actionImpl.AddAttachmentToSaved
} as Action)
const showMenu = (ev: Event) => {
showPopup(
Menu,
{
actions: [
{
label: saveAttachmentAction.label,
icon: saveAttachmentAction.icon,
action: async (evt: MouseEvent) => {
const impl = await getResource(saveAttachmentAction.action)
await impl(value, evt)
}
}
]
},
ev.target as HTMLElement
)
}
</script> </script>
<div class="flex-row-center"> <div class="flex-row-center">
{#if type === 'image'} {#if type === 'image'}
<div class='content flex-center cursor-pointer' on:click={() => { <div
closeTooltip() class="content flex-center buttonContainer cursor-pointer"
showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right') on:click={() => {
}}> closeTooltip()
showPopup(PDFViewer, { file: value.file, name: value.name, contentType: value.type }, 'right')
}}
>
<img src={getFileUrl(value.file)} alt={value.name} /> <img src={getFileUrl(value.file)} alt={value.name} />
<div class="more">
<ActionIcon
icon={IconMoreH}
size={'small'}
action={(e) => {
showMenu(e)
}}
/>
</div>
</div> </div>
{:else if type === 'audio'} {:else if type === 'audio'}
<AudioPlayer {value} /> <div class="buttonContainer">
<AudioPlayer {value} />
<div class="more">
<ActionIcon
icon={IconMoreH}
size={'small'}
action={(e) => {
showMenu(e)
}}
/>
</div>
</div>
{:else if type === 'video'} {:else if type === 'video'}
<div class='content flex-center'> <div class="content buttonContainer flex-center">
<video controls> <video controls>
<source src={getFileUrl(value.file)} type={value.type}> <source src={getFileUrl(value.file)} type={value.type} />
<track kind="captions" label={value.name} /> <track kind="captions" label={value.name} />
<div class='container'> <div class="container">
<AttachmentPresenter {value} /> <AttachmentPresenter {value} />
</div> </div>
</video> </video>
<div class="more">
<ActionIcon
icon={IconMoreH}
size={'small'}
action={(e) => {
showMenu(e)
}}
/>
</div>
</div> </div>
{:else} {:else}
<div class='container'> <div class="flex container buttonContainer">
<AttachmentPresenter {value} /> <AttachmentPresenter {value} />
<div class="more">
<ActionIcon
icon={IconMoreH}
size={'small'}
action={(e) => {
showMenu(e)
}}
/>
</div>
</div> </div>
{/if} {/if}
</div> </div>
@ -59,14 +132,29 @@
background-color: var(--theme-bg-accent-color); background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-bg-accent-color); border: 1px solid var(--theme-bg-accent-color);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 0.5rem padding: 0.5rem;
}
.buttonContainer {
align-items: flex-start;
.more {
margin-left: 0.5rem;
visibility: hidden;
}
}
.buttonContainer:hover {
.more {
visibility: visible;
}
} }
.content { .content {
max-width: 20rem; max-width: 20rem;
max-height: 20rem; max-height: 20rem;
img, video { img,
video {
max-width: 20rem; max-width: 20rem;
max-height: 20rem; max-height: 20rem;
border-radius: 0.75rem; border-radius: 0.75rem;

View File

@ -26,6 +26,9 @@ import FileBrowser from './components/FileBrowser.svelte'
import Photos from './components/Photos.svelte' import Photos from './components/Photos.svelte'
import { Resources } from '@anticrm/platform' import { Resources } from '@anticrm/platform'
import { uploadFile, deleteFile } from './utils' import { uploadFile, deleteFile } from './utils'
import attachment, { Attachment } from '@anticrm/attachment'
import preference from '@anticrm/preference'
import { getClient } from '@anticrm/presentation'
export { export {
AddAttachment, AddAttachment,
@ -38,6 +41,23 @@ export {
AttachmentDocList AttachmentDocList
} }
export async function AddAttachmentToSaved(attach: Attachment): Promise<void> {
const client = getClient()
await client.createDoc(attachment.class.SavedAttachments, preference.space.Preference, {
attachedTo: attach._id
})
}
export async function DeleteAttachmentFromSaved(attach: Attachment): Promise<void> {
const client = getClient()
const current = await client.findOne(attachment.class.SavedAttachments, { attachedTo: attach._id })
if (current !== undefined) {
await client.remove(current)
}
}
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
component: { component: {
AttachmentsPresenter, AttachmentsPresenter,
@ -52,5 +72,9 @@ export default async (): Promise<Resources> => ({
helper: { helper: {
UploadFile: uploadFile, UploadFile: uploadFile,
DeleteFile: deleteFile DeleteFile: deleteFile
},
actionImpl: {
AddAttachmentToSaved,
DeleteAttachmentFromSaved
} }
}) })

View File

@ -18,6 +18,7 @@ import { mergeIds } from '@anticrm/platform'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import attachment, { attachmentId } from '@anticrm/attachment' import attachment, { attachmentId } from '@anticrm/attachment'
import { ViewAction } from '@anticrm/view'
export default mergeIds(attachmentId, attachment, { export default mergeIds(attachmentId, attachment, {
string: { string: {
@ -46,6 +47,12 @@ export default mergeIds(attachmentId, attachment, {
FileBrowserTypeFilterImages: '' as IntlString, FileBrowserTypeFilterImages: '' as IntlString,
FileBrowserTypeFilterAudio: '' as IntlString, FileBrowserTypeFilterAudio: '' as IntlString,
FileBrowserTypeFilterVideos: '' as IntlString, FileBrowserTypeFilterVideos: '' as IntlString,
FileBrowserTypeFilterPDFs: '' as IntlString FileBrowserTypeFilterPDFs: '' as IntlString,
AddAttachmentToSaved: '' as IntlString,
RemoveAttachmentFromSaved: '' as IntlString
},
actionImpl: {
AddAttachmentToSaved: '' as ViewAction,
DeleteAttachmentFromSaved: '' as ViewAction
} }
}) })

View File

@ -28,6 +28,7 @@
"dependencies": { "dependencies": {
"@anticrm/platform": "~0.6.5", "@anticrm/platform": "~0.6.5",
"@anticrm/ui": "~0.6.0", "@anticrm/ui": "~0.6.0",
"@anticrm/core": "~0.6.16" "@anticrm/core": "~0.6.16",
"@anticrm/preference": "~0.6.0"
} }
} }

View File

@ -17,6 +17,7 @@
import type { AttachedDoc, Class, Doc, Ref, Space } from '@anticrm/core' import type { AttachedDoc, Class, Doc, Ref, Space } from '@anticrm/core'
import type { Asset, Plugin } from '@anticrm/platform' import type { Asset, Plugin } from '@anticrm/platform'
import { IntlString, plugin, Resource } from '@anticrm/platform' import { IntlString, plugin, Resource } from '@anticrm/platform'
import type { Preference } from '@anticrm/preference'
import { AnyComponent } from '@anticrm/ui' import { AnyComponent } from '@anticrm/ui'
/** /**
@ -35,6 +36,13 @@ export interface Attachment extends AttachedDoc {
*/ */
export interface Photo extends Attachment {} export interface Photo extends Attachment {}
/**
* @public
*/
export interface SavedAttachments extends Preference {
attachedTo: Ref<Attachment>
}
/** /**
* @public * @public
*/ */
@ -52,7 +60,8 @@ export default plugin(attachmentId, {
}, },
class: { class: {
Attachment: '' as Ref<Class<Attachment>>, Attachment: '' as Ref<Class<Attachment>>,
Photo: '' as Ref<Class<Photo>> Photo: '' as Ref<Class<Photo>>,
SavedAttachments: '' as Ref<Class<SavedAttachments>>
}, },
helper: { helper: {
UploadFile: '' as Resource<(file: File, opts?: { space: Ref<Space>, attachedTo: Ref<Doc> }) => Promise<string>>, UploadFile: '' as Resource<(file: File, opts?: { space: Ref<Space>, attachedTo: Ref<Doc> }) => Promise<string>>,

View File

@ -55,6 +55,7 @@
"RemoveFromSaved": "Remove from saved", "RemoveFromSaved": "Remove from saved",
"EmptySavedHeader": "Add messages to come back to later", "EmptySavedHeader": "Add messages to come back to later",
"EmptySavedText": "Tick off your to-dos or save something for another time. Only you can see your saved items, so use them however you like.", "EmptySavedText": "Tick off your to-dos or save something for another time. Only you can see your saved items, so use them however you like.",
"SharedBy": "Shared by {name} {time}",
"LeaveChannel": "Leave channel", "LeaveChannel": "Leave channel",
"ChannelBrowser": "Channel browser" "ChannelBrowser": "Channel browser"
} }

View File

@ -54,6 +54,7 @@
"RemoveFromSaved": "Удалить из сохраненных", "RemoveFromSaved": "Удалить из сохраненных",
"EmptySavedHeader": "Добавляйте сообщения и файлы, чтобы вернуться к ним позже", "EmptySavedHeader": "Добавляйте сообщения и файлы, чтобы вернуться к ним позже",
"EmptySavedText": "Пометьте свои задачи или сохраните что-нибудь на потом. Только вы можете просматривать свои сохраненные объекты, поэтому используйте их как угодно.", "EmptySavedText": "Пометьте свои задачи или сохраните что-нибудь на потом. Только вы можете просматривать свои сохраненные объекты, поэтому используйте их как угодно.",
"SharedBy": "Доступ открыт {name} {time}",
"LeaveChannel": "Покинуть канал", "LeaveChannel": "Покинуть канал",
"ChannelBrowser": "Браузер каналов" "ChannelBrowser": "Браузер каналов"
} }

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import attachment from '@anticrm/attachment' import attachment, { Attachment } from '@anticrm/attachment'
import type { ChunterMessage, Message } from '@anticrm/chunter' import type { ChunterMessage, Message } from '@anticrm/chunter'
import contact, { Employee } from '@anticrm/contact' import contact, { Employee } from '@anticrm/contact'
import core, { Doc, Ref, Space, WithLookup } from '@anticrm/core' import core, { Doc, Ref, Space, WithLookup } from '@anticrm/core'
@ -26,7 +26,8 @@
export let space: Ref<Space> | undefined export let space: Ref<Space> | undefined
export let pinnedIds: Ref<ChunterMessage>[] export let pinnedIds: Ref<ChunterMessage>[]
export let savedIds: Ref<ChunterMessage>[] export let savedMessagesIds: Ref<ChunterMessage>[]
export let savedAttachmentsIds: Ref<Attachment>[]
let div: HTMLDivElement | undefined let div: HTMLDivElement | undefined
let autoscroll: boolean = false let autoscroll: boolean = false
@ -119,7 +120,8 @@
{employees} {employees}
on:openThread on:openThread
isPinned={pinnedIds.includes(message._id)} isPinned={pinnedIds.includes(message._id)}
isSaved={savedIds.includes(message._id)} isSaved={savedMessagesIds.includes(message._id)}
{savedAttachmentsIds}
/> />
{/each} {/each}
{/if} {/if}

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources' import { AttachmentRefInput } from '@anticrm/attachment-resources'
import { ChunterMessage, Message, ChunterSpace } from '@anticrm/chunter' import { ChunterMessage, Message, ChunterSpace } from '@anticrm/chunter'
import { generateId, getCurrentAccount, Ref, Space, TxFactory } from '@anticrm/core' import { generateId, getCurrentAccount, Ref, Space, TxFactory } from '@anticrm/core'
@ -99,10 +100,16 @@
{ limit: 1 } { limit: 1 }
) )
const preferenceQuery = createQuery() const savedMessagesQuery = createQuery()
let savedIds: Ref<ChunterMessage>[] = [] let savedMessagesIds: Ref<ChunterMessage>[] = []
preferenceQuery.query(chunter.class.SavedMessages, {}, (res) => { savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
savedIds = res.map((r) => r.attachedTo) savedMessagesIds = res.map((r) => r.attachedTo)
})
const savedAttachmentsQuery = createQuery()
let savedAttachmentsIds: Ref<Attachment>[] = []
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
savedAttachmentsIds = res.map((r) => r.attachedTo)
}) })
</script> </script>
@ -113,7 +120,8 @@
openThread(e.detail) openThread(e.detail)
}} }}
{pinnedIds} {pinnedIds}
{savedIds} {savedMessagesIds}
{savedAttachmentsIds}
/> />
<div class="reference"> <div class="reference">
<AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage} /> <AttachmentRefInput {space} {_class} objectId={_id} on:message={onMessage} />

View File

@ -25,7 +25,7 @@
import { Action } from '@anticrm/view' import { Action } from '@anticrm/view'
import { getActions } from '@anticrm/view-resources' import { getActions } from '@anticrm/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { AddToSaved, DeleteFromSaved, UnpinMessage } from '../index' import { AddMessageToSaved, DeleteMessageFromSaved, UnpinMessage } from '../index'
import chunter from '../plugin' import chunter from '../plugin'
import { getTime } from '../utils' import { getTime } from '../utils'
// import Share from './icons/Share.svelte' // import Share from './icons/Share.svelte'
@ -37,6 +37,7 @@
export let message: WithLookup<ChunterMessage> export let message: WithLookup<ChunterMessage>
export let employees: Map<Ref<Employee>, Employee> export let employees: Map<Ref<Employee>, Employee>
export let savedAttachmentsIds: Ref<Attachment>[]
export let thread: boolean = false export let thread: boolean = false
export let isPinned: boolean = false export let isPinned: boolean = false
export let isSaved: boolean = false export let isSaved: boolean = false
@ -86,7 +87,7 @@
action: async () => { action: async () => {
;(await client.findAll(chunter.class.ThreadMessage, { attachedTo: message._id as Ref<Message> })).forEach((c) => { ;(await client.findAll(chunter.class.ThreadMessage, { attachedTo: message._id as Ref<Message> })).forEach((c) => {
UnpinMessage(c) UnpinMessage(c)
DeleteFromSaved(c) DeleteMessageFromSaved(c)
}) })
UnpinMessage(message) UnpinMessage(message)
await client.removeDoc(message._class, message.space, message._id) await client.removeDoc(message._class, message.space, message._id)
@ -108,7 +109,7 @@
...actions.map((a) => ({ ...actions.map((a) => ({
label: a.label, label: a.label,
icon: a.icon, icon: a.icon,
action: async (ctx:any, evt: MouseEvent) => { action: async (ctx: any, evt: MouseEvent) => {
const impl = await getResource(a.action) const impl = await getResource(a.action)
await impl(message, evt) await impl(message, evt)
} }
@ -145,8 +146,8 @@
} }
function addToSaved () { function addToSaved () {
if (isSaved) DeleteFromSaved(message) if (isSaved) DeleteMessageFromSaved(message)
else AddToSaved(message) else AddMessageToSaved(message)
} }
$: parentMessage = message as Message $: parentMessage = message as Message
@ -183,7 +184,11 @@
</div> </div>
{:else} {:else}
<div class="text"><MessageViewer message={message.content} /></div> <div class="text"><MessageViewer message={message.content} /></div>
{#if message.attachments}<div class="attachments"><AttachmentList {attachments} /></div>{/if} {#if message.attachments}
<div class="attachments">
<AttachmentList {attachments} {savedAttachmentsIds} />
</div>
{/if}
{/if} {/if}
{#if reactions || (!thread && hasReplies)} {#if reactions || (!thread && hasReplies)}
<div class="footer flex-col"> <div class="footer flex-col">

View File

@ -1,27 +1,63 @@
<script lang="ts"> <script lang="ts">
import { createQuery } from '@anticrm/presentation' import attachment, { Attachment } from '@anticrm/attachment'
import { createQuery, getClient } from '@anticrm/presentation'
import { ChunterMessage } from '@anticrm/chunter' import { ChunterMessage } from '@anticrm/chunter'
import { Ref } from '@anticrm/core' import core, { Ref, WithLookup } from '@anticrm/core'
import Message from './Message.svelte' import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
import contact, { Employee } from '@anticrm/contact' import { getCurrentLocation, Label, navigate, Scroller } from '@anticrm/ui'
import { getCurrentLocation, Label, navigate } from '@anticrm/ui' import AttachmentPreview from '@anticrm/attachment-resources/src/components/AttachmentPreview.svelte'
import Bookmark from './icons/Bookmark.svelte' import Bookmark from './icons/Bookmark.svelte'
import Message from './Message.svelte'
import chunter from '../plugin' import chunter from '../plugin'
import { getTime } from '../utils'
let savedIds: Ref<ChunterMessage>[] = [] const client = getClient()
let savedMessages: ChunterMessage[] = [] let savedMessagesIds: Ref<ChunterMessage>[] = []
let savedMessages: WithLookup<ChunterMessage>[] = []
let savedAttachmentsIds: Ref<Attachment>[] = []
let savedAttachments: WithLookup<Attachment>[] = []
const messagesQuery = createQuery() const messagesQuery = createQuery()
const preferenceQuery = createQuery() const attachmentsQuery = createQuery()
const savedMessagesQuery = createQuery()
const savedAttachmentsQuery = createQuery()
preferenceQuery.query(chunter.class.SavedMessages, {}, (res) => { savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
savedIds = res.map((r) => r.attachedTo) savedMessagesIds = res.map((r) => r.attachedTo)
}) })
$: savedIds && savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
messagesQuery.query(chunter.class.ChunterMessage, { _id: { $in: savedIds } }, (res) => { savedAttachmentsIds = res.map((r) => r.attachedTo)
savedMessages = res })
})
$: savedMessagesIds &&
messagesQuery.query(
chunter.class.ChunterMessage,
{ _id: { $in: savedMessagesIds } },
(res) => {
savedMessages = res
},
{
lookup: {
_id: { attachments: attachment.class.Attachment },
createBy: core.class.Account
}
}
)
$: savedAttachmentsIds &&
attachmentsQuery.query(
attachment.class.Attachment,
{ _id: { $in: savedAttachmentsIds } },
(res) => {
savedAttachments = res
},
{
lookup: {
modifiedBy: core.class.Account
}
}
)
let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>() let employees: Map<Ref<Employee>, Employee> = new Map<Ref<Employee>, Employee>()
const employeeQuery = createQuery() const employeeQuery = createQuery()
@ -52,7 +88,7 @@
function openMessage (message: ChunterMessage) { function openMessage (message: ChunterMessage) {
const loc = getCurrentLocation() const loc = getCurrentLocation()
if (message.attachedToClass === chunter.class.Channel) { if (message.attachedToClass === chunter.class.ChunterSpace) {
loc.path.length = 3 loc.path.length = 3
loc.path[2] = message.attachedTo loc.path[2] = message.attachedTo
} else if (message.attachedToClass === chunter.class.Message) { } else if (message.attachedToClass === chunter.class.Message) {
@ -62,32 +98,57 @@
} }
navigate(loc) 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)
})
}
function getName (a: WithLookup<Attachment>): string | undefined {
const name = (a.$lookup?.modifiedBy as EmployeeAccount).name
if (name !== undefined) {
return formatName(name)
}
}
</script> </script>
{#if savedMessages.length > 0} <Scroller>
{#each savedMessages as message} {#if savedMessages.length > 0 || savedAttachments.length > 0}
<div on:click={() => openMessage(message)}> {#each savedMessages as message}
<Message <div on:click={() => openMessage(message)}>
{message} <Message
{employees} {message}
on:openThread {employees}
thread on:openThread
isPinned={pinnedIds.includes(message._id)} thread
isSaved={savedIds.includes(message._id)} isPinned={pinnedIds.includes(message._id)}
/> isSaved={savedMessagesIds.includes(message._id)}
{savedAttachmentsIds}
/>
</div>
{/each}
{#each savedAttachments as att}
<div class="attachmentContainer" on:click={() => openAttachment(att)}>
<AttachmentPreview value={att} isSaved={true} />
<div class="label">
<Label label={chunter.string.SharedBy} params={{ name: getName(att), time: getTime(att.modifiedOn) }} />
</div>
</div>
{/each}
{:else}
<div class="empty">
<Bookmark size={'large'} />
<div class="an-element__label header">
<Label label={chunter.string.EmptySavedHeader} />
</div>
<span class="an-element__label">
<Label label={chunter.string.EmptySavedText} />
</span>
</div> </div>
{/each} {/if}
{:else} </Scroller>
<div class="empty">
<Bookmark size={'large'} />
<div class="an-element__label header">
<Label label={chunter.string.EmptySavedHeader} />
</div>
<span class="an-element__label">
<Label label={chunter.string.EmptySavedText} />
</span>
</div>
{/if}
<style lang="scss"> <style lang="scss">
.empty { .empty {
@ -105,4 +166,16 @@
font-weight: 600; font-weight: 600;
margin: 1rem; margin: 1rem;
} }
.attachmentContainer {
padding: 2rem;
&:hover {
background-color: var(--board-card-bg-hover);
}
.label {
padding-top: 1rem;
}
}
</style> </style>

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import attachment from '@anticrm/attachment' import attachment, { Attachment } from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources' import { AttachmentRefInput } from '@anticrm/attachment-resources'
import type { ChunterSpace, Message, ThreadMessage } from '@anticrm/chunter' import type { ChunterSpace, Message, ThreadMessage } from '@anticrm/chunter'
import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact' import contact, { Employee, EmployeeAccount, formatName } from '@anticrm/contact'
@ -31,6 +31,7 @@
const query = createQuery() const query = createQuery()
const messageQuery = createQuery() const messageQuery = createQuery()
export let savedAttachmentsIds: Ref<Attachment>[]
export let _id: Ref<Message> export let _id: Ref<Message>
let parent: Message | undefined let parent: Message | undefined
let commentId = generateId() as Ref<ThreadMessage> let commentId = generateId() as Ref<ThreadMessage>
@ -181,7 +182,7 @@
</div> </div>
<div class="flex-col content"> <div class="flex-col content">
{#if parent} {#if parent}
<MsgView message={parent} {employees} thread /> <MsgView message={parent} {employees} thread {savedAttachmentsIds} />
{#if total > comments.length} {#if total > comments.length}
<div <div
class="label pb-2 pt-2 pl-8 over-underline" class="label pb-2 pt-2 pl-8 over-underline"
@ -193,7 +194,7 @@
</div> </div>
{/if} {/if}
{#each comments as comment (comment._id)} {#each comments as comment (comment._id)}
<MsgView message={comment} {employees} thread /> <MsgView message={comment} {employees} thread {savedAttachmentsIds} />
{/each} {/each}
<div class="mr-4 ml-4 mb-4 mt-2"> <div class="mr-4 ml-4 mb-4 mt-2">
<AttachmentRefInput <AttachmentRefInput

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import attachment from '@anticrm/attachment' import attachment, { Attachment } from '@anticrm/attachment'
import { AttachmentRefInput } from '@anticrm/attachment-resources' import { AttachmentRefInput } from '@anticrm/attachment-resources'
import type { ThreadMessage, Message, ChunterMessage } from '@anticrm/chunter' import type { ThreadMessage, Message, ChunterMessage } from '@anticrm/chunter'
import contact, { Employee } from '@anticrm/contact' import contact, { Employee } from '@anticrm/contact'
@ -123,11 +123,17 @@
)) ))
) )
const preferenceQuery = createQuery() const savedMessagesQuery = createQuery()
let savedIds: Ref<ChunterMessage>[] = [] let savedMessagesIds: Ref<ChunterMessage>[] = []
preferenceQuery.query(chunter.class.SavedMessages, {}, (res) => { savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
savedIds = res.map((r) => r.attachedTo) savedMessagesIds = res.map((r) => r.attachedTo)
})
const savedAttachmentsQuery = createQuery()
let savedAttachmentsIds: Ref<Attachment>[] = []
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
savedAttachmentsIds = res.map((r) => r.attachedTo)
}) })
async function onMessage (event: CustomEvent) { async function onMessage (event: CustomEvent) {
@ -192,7 +198,7 @@
</div> </div>
<div class="flex-col vScroll content" bind:this={div}> <div class="flex-col vScroll content" bind:this={div}>
{#if message} {#if message}
<MsgView {message} {employees} thread /> <MsgView {message} {employees} thread isSaved={savedMessagesIds.includes(message._id)} {savedAttachmentsIds} />
{#if comments.length} {#if comments.length}
<ChannelSeparator title={chunter.string.RepliesCount} line params={{ replies: comments.length }} /> <ChannelSeparator title={chunter.string.RepliesCount} line params={{ replies: comments.length }} />
{/if} {/if}
@ -205,7 +211,8 @@
{employees} {employees}
thread thread
isPinned={pinnedIds.includes(comment._id)} isPinned={pinnedIds.includes(comment._id)}
isSaved={savedIds.includes(comment._id)} isSaved={savedMessagesIds.includes(comment._id)}
{savedAttachmentsIds}
/> />
{/each} {/each}
{/if} {/if}

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@anticrm/attachment'
import type { Message } from '@anticrm/chunter' import type { Message } from '@anticrm/chunter'
import { getCurrentAccount, Ref, SortingOrder } from '@anticrm/core' import { getCurrentAccount, Ref, SortingOrder } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation' import { createQuery } from '@anticrm/presentation'
@ -40,6 +41,12 @@
} }
} }
) )
const savedAttachmentsQuery = createQuery()
let savedAttachmentsIds: Ref<Attachment>[] = []
savedAttachmentsQuery.query(attachment.class.SavedAttachments, {}, (res) => {
savedAttachmentsIds = res.map((r) => r.attachedTo)
})
</script> </script>
<div class="ac-header full divide"> <div class="ac-header full divide">
@ -49,7 +56,7 @@
</div> </div>
<Scroller> <Scroller>
{#each threads as thread (thread)} {#each threads as thread (thread)}
<div class="item"><Thread _id={thread} /></div> <div class="item"><Thread _id={thread} {savedAttachmentsIds} /></div>
{/each} {/each}
</Scroller> </Scroller>

View File

@ -17,6 +17,7 @@ import core from '@anticrm/core'
import chunter, { ChunterSpace, Channel, ChunterMessage, Message, ThreadMessage } from '@anticrm/chunter' import chunter, { ChunterSpace, Channel, ChunterMessage, Message, ThreadMessage } from '@anticrm/chunter'
import { NotificationClientImpl } from '@anticrm/notification-resources' import { NotificationClientImpl } from '@anticrm/notification-resources'
import { Resources } from '@anticrm/platform' import { Resources } from '@anticrm/platform'
import preference from '@anticrm/preference'
import { getClient, MessageBox } from '@anticrm/presentation' import { getClient, MessageBox } from '@anticrm/presentation'
import { getCurrentLocation, navigate, showPopup } from '@anticrm/ui' import { getCurrentLocation, navigate, showPopup } from '@anticrm/ui'
import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte' import TxBacklinkCreate from './components/activity/TxBacklinkCreate.svelte'
@ -36,7 +37,6 @@ import EditChannel from './components/EditChannel.svelte'
import ThreadView from './components/ThreadView.svelte' import ThreadView from './components/ThreadView.svelte'
import Threads from './components/Threads.svelte' import Threads from './components/Threads.svelte'
import SavedMessages from './components/SavedMessages.svelte' import SavedMessages from './components/SavedMessages.svelte'
import preference from '@anticrm/preference'
import { getDmName } from './utils' import { getDmName } from './utils'
@ -135,7 +135,7 @@ async function UnarchiveChannel (channel: Channel): Promise<void> {
) )
} }
export async function AddToSaved (message: ChunterMessage): Promise<void> { export async function AddMessageToSaved (message: ChunterMessage): Promise<void> {
const client = getClient() const client = getClient()
await client.createDoc(chunter.class.SavedMessages, preference.space.Preference, { await client.createDoc(chunter.class.SavedMessages, preference.space.Preference, {
@ -143,7 +143,7 @@ export async function AddToSaved (message: ChunterMessage): Promise<void> {
}) })
} }
export async function DeleteFromSaved (message: ChunterMessage): Promise<void> { export async function DeleteMessageFromSaved (message: ChunterMessage): Promise<void> {
const client = getClient() const client = getClient()
const current = await client.findOne(chunter.class.SavedMessages, { attachedTo: message._id }) const current = await client.findOne(chunter.class.SavedMessages, { attachedTo: message._id })

View File

@ -77,6 +77,7 @@ export default mergeIds(chunterId, chunter, {
RemoveFromSaved: '' as IntlString, RemoveFromSaved: '' as IntlString,
EmptySavedHeader: '' as IntlString, EmptySavedHeader: '' as IntlString,
EmptySavedText: '' as IntlString, EmptySavedText: '' as IntlString,
SharedBy: '' as IntlString,
LeaveChannel: '' as IntlString, LeaveChannel: '' as IntlString,
ChannelBrowser: '' as IntlString ChannelBrowser: '' as IntlString
} }