mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-22 08:20:39 +00:00
UBERF-5315: update chat (#4572)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
baa8eb3752
commit
643f34bf96
@ -425,7 +425,7 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
textProvider: chunter.function.GetLink
|
||||
},
|
||||
label: chunter.string.CopyLink,
|
||||
icon: chunter.icon.Thread,
|
||||
icon: chunter.icon.Copy,
|
||||
keyBinding: [],
|
||||
input: 'none',
|
||||
category: chunter.category.Chunter,
|
||||
@ -693,6 +693,14 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
ofMessage: activity.class.ActivityInfoMessage,
|
||||
components: [{ kind: 'footer', component: chunter.component.Replies }]
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Channel, core.class.Class, chunter.mixin.ObjectChatPanel, {
|
||||
ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description']
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.DirectMessage, core.class.Class, chunter.mixin.ObjectChatPanel, {
|
||||
ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description']
|
||||
})
|
||||
}
|
||||
|
||||
export default chunter
|
||||
|
@ -58,6 +58,14 @@ export function createModel (builder: Builder): void {
|
||||
trigger: serverChunter.trigger.ChunterTrigger
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverChunter.trigger.OnChannelMembersChanged,
|
||||
txMatch: {
|
||||
_class: core.class.TxUpdateDoc,
|
||||
objectClass: chunter.class.Channel
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverChunter.trigger.OnDirectMessageSent,
|
||||
txMatch: {
|
||||
|
@ -13,3 +13,13 @@ export function groupByArray<T, K> (array: T[], keyProvider: (item: T) => K): Ma
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function flipSet<T> (set: Set<T>, item: T): Set<T> {
|
||||
if (set.has(item)) {
|
||||
set.delete(item)
|
||||
} else {
|
||||
set.add(item)
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
@ -29,6 +29,7 @@
|
||||
"MakePrivate": "Make private",
|
||||
"MakePrivateDescription": "Only members can see it",
|
||||
"Created": "Created",
|
||||
"NoResults": "No results to show"
|
||||
"NoResults": "No results to show",
|
||||
"Next": "Next"
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@
|
||||
"MakePrivate": "Сделать личным",
|
||||
"MakePrivateDescription": "Только пользователи могут видеть это",
|
||||
"Created": "Созданные",
|
||||
"NoResults": "Нет результатов"
|
||||
"NoResults": "Нет результатов",
|
||||
"Next": "Далее"
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +70,8 @@ export default plugin(presentationId, {
|
||||
MakePrivateDescription: '' as IntlString,
|
||||
OpenInANewTab: '' as IntlString,
|
||||
Created: '' as IntlString,
|
||||
NoResults: '' as IntlString
|
||||
NoResults: '' as IntlString,
|
||||
Next: '' as IntlString
|
||||
},
|
||||
metadata: {
|
||||
RequiredVersion: '' as Metadata<string>,
|
||||
|
@ -749,6 +749,10 @@ input.search {
|
||||
.square-36 { width: 2.25rem; height: 2.25rem; }
|
||||
|
||||
/* --------- */
|
||||
.svg-xx-small {
|
||||
width: .5rem;
|
||||
height: .5rem;
|
||||
}
|
||||
.svg-tiny {
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
@ -789,7 +793,7 @@ input.search {
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
}
|
||||
.svg-card, .svg-x-small, .svg-small, .svg-medium, .svg-large, .svg-x-large { flex-shrink: 0; }
|
||||
.svg-card, .svg-xx-small, .svg-x-small, .svg-small, .svg-medium, .svg-large, .svg-x-large { flex-shrink: 0; }
|
||||
|
||||
.svg-mask {
|
||||
position: absolute;
|
||||
|
@ -184,6 +184,11 @@
|
||||
.hulyModal-container {
|
||||
height: 100%;
|
||||
border-top: 1px solid transparent;
|
||||
visibility: visible;
|
||||
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.hulyModal-content {
|
||||
height: 100%;
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { AnySvelteComponent, LabelAndProps } from '../types'
|
||||
import { AnySvelteComponent, IconSize, LabelAndProps } from '../types'
|
||||
import { tooltip as tp } from '../tooltips'
|
||||
import { ComponentType } from 'svelte'
|
||||
import Spinner from './Spinner.svelte'
|
||||
@ -25,6 +25,7 @@
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let labelParams: Record<string, any> = {}
|
||||
export let icon: Asset | AnySvelteComponent | ComponentType | undefined = undefined
|
||||
export let iconSize: IconSize | undefined = undefined
|
||||
export let iconProps: any | undefined = undefined
|
||||
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative'
|
||||
export let size: 'large' | 'medium' | 'small' | 'extra-small'
|
||||
@ -37,6 +38,14 @@
|
||||
export let inheritFont: boolean = false
|
||||
export let tooltip: LabelAndProps | undefined = undefined
|
||||
export let element: HTMLButtonElement | undefined = undefined
|
||||
|
||||
let actualIconSize: IconSize = 'small'
|
||||
|
||||
$: if (iconSize) {
|
||||
actualIconSize = iconSize
|
||||
} else if (type === 'type-button' && !hasMenu) {
|
||||
actualIconSize = 'medium'
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
@ -54,7 +63,7 @@
|
||||
{#if loading}
|
||||
<div class="icon animate"><Spinner size={type === 'type-button' && !hasMenu ? 'medium' : 'small'} /></div>
|
||||
{:else if icon}<div class="icon">
|
||||
<Icon {icon} {iconProps} size={type === 'type-button' && !hasMenu ? 'medium' : 'small'} />
|
||||
<Icon {icon} {iconProps} size={actualIconSize} />
|
||||
</div>{/if}
|
||||
{#if label}<span><Label {label} params={labelParams} /></span>{/if}
|
||||
{#if title}<span>{title}</span>{/if}
|
||||
|
@ -29,6 +29,8 @@
|
||||
export let onCancel: (() => void) | undefined = undefined
|
||||
export let canSave: boolean = false
|
||||
export let okLabel: IntlString = ui.string.Ok
|
||||
export let padding: string | undefined = undefined
|
||||
export let hidden = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -39,11 +41,18 @@
|
||||
function onKeyDown (ev: KeyboardEvent) {
|
||||
if (ev.key === 'Escape') close()
|
||||
}
|
||||
|
||||
$: typePadding =
|
||||
type === 'type-popup'
|
||||
? 'var(--spacing-2) var(--spacing-3) var(--spacing-4)'
|
||||
: type === 'type-aside'
|
||||
? 'var(--spacing-2) var(--spacing-1_5)'
|
||||
: 'var(--spacing-3)'
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
|
||||
<div class="hulyModal-container {type}">
|
||||
<div class="hulyModal-container {type}" class:hidden>
|
||||
<Header {type} on:close={close}>
|
||||
<Label {label} params={labelProps} />
|
||||
<svelte:fragment slot="actions">
|
||||
@ -52,11 +61,7 @@
|
||||
</Header>
|
||||
<div class="hulyModal-content">
|
||||
<Scroller
|
||||
padding={type === 'type-popup'
|
||||
? 'var(--spacing-2) var(--spacing-3) var(--spacing-4)'
|
||||
: type === 'type-aside'
|
||||
? 'var(--spacing-2) var(--spacing-1_5)'
|
||||
: 'var(--spacing-3)'}
|
||||
padding={padding ?? typePadding}
|
||||
bottomPadding={type === 'type-popup'
|
||||
? undefined
|
||||
: type === 'type-aside'
|
||||
|
@ -6,12 +6,14 @@
|
||||
export let kind: 'separated' | 'separated-free' = 'separated'
|
||||
export let expansion: 'stretch' | 'default' = 'default'
|
||||
export let padding: string | undefined = undefined
|
||||
export let notifyFor: IModeSelector['mode'][] = []
|
||||
|
||||
$: modeList = props.config.map((c) => {
|
||||
return {
|
||||
id: c[0],
|
||||
labelIntl: c[1],
|
||||
labelParams: c[2],
|
||||
showNotify: notifyFor.includes(c[0]),
|
||||
action: () => {
|
||||
props.onChange(c[0])
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
//
|
||||
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { AnySvelteComponent } from '../types'
|
||||
import { AnySvelteComponent, IconSize } from '../types'
|
||||
import { ComponentType } from 'svelte'
|
||||
import ButtonBase from './ButtonBase.svelte'
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
|
||||
export let size: 'large' | 'medium' | 'small' = 'large'
|
||||
export let icon: Asset | AnySvelteComponent | ComponentType | undefined = undefined
|
||||
export let iconSize: IconSize | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
export let loading: boolean = false
|
||||
export let hasMenu: boolean = false
|
||||
@ -29,6 +30,7 @@
|
||||
{kind}
|
||||
{size}
|
||||
{icon}
|
||||
{iconSize}
|
||||
{loading}
|
||||
{disabled}
|
||||
{hasMenu}
|
||||
|
@ -102,12 +102,16 @@
|
||||
<div class="color" style:background-color={item.color} />
|
||||
{/if}
|
||||
{#if item.label || item.labelIntl}
|
||||
<span class="overflow-label" class:ml-1-5={item.icon || item.color}>
|
||||
<span class="flex-center overflow-label" class:ml-1-5={item.icon || item.color}>
|
||||
{#if item.label}
|
||||
{item.label}
|
||||
{:else if item.labelIntl}
|
||||
<Label label={item.labelIntl} params={item.labelParams} />
|
||||
{/if}
|
||||
|
||||
{#if item.showNotify}
|
||||
<div class="notifyMarker" />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@ -285,4 +289,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notifyMarker {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 1px;
|
||||
background: var(--global-higlight-Color);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -20,10 +20,10 @@
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 14 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16 6C9.20948 6 4 10.6272 4 16C4 18.1619 4.82539 20.1782 6.25857 21.8353C6.48585 22.0981 6.56044 22.46 6.45559 22.7913C6.11025 23.8822 5.59859 24.9049 4.96437 25.8562C6.36749 25.6496 7.72455 25.2236 8.98685 24.5982C9.26684 24.4595 9.59558 24.4596 9.87546 24.5986C11.6632 25.4861 13.7558 26 16 26C22.7905 26 28 21.3728 28 16C28 10.6272 22.7905 6 16 6ZM2 16C2 9.2225 8.43112 4 16 4C23.5689 4 30 9.2225 30 16C30 22.7775 23.5689 28 16 28C13.6319 28 11.3944 27.4963 9.43078 26.6016C7.4311 27.5144 5.24057 28 3 28C2.63121 28 2.29235 27.797 2.11833 27.4719C1.94431 27.1467 1.96338 26.7522 2.16795 26.4453L3.37487 24.6349C3.78033 24.0267 4.11535 23.3749 4.37375 22.692C2.88402 20.7925 2 18.4911 2 16Z"
|
||||
d="M7 1C3.60474 1 1 3.31362 1 6C1 7.08097 1.4127 8.08912 2.12929 8.91766C2.24292 9.04906 2.28022 9.23002 2.22779 9.39564C2.05513 9.9411 1.7993 10.4525 1.48219 10.9281C2.18374 10.8248 2.86228 10.6118 3.49342 10.2991C3.63342 10.2297 3.79779 10.2298 3.93773 10.2993C4.83161 10.7431 5.87792 11 7 11C10.3953 11 13 8.68638 13 6C13 3.31362 10.3953 1 7 1ZM7.77784e-07 6C7.77784e-07 2.61125 3.21556 0 7 0C10.7844 0 14 2.61125 14 6C14 9.38875 10.7844 12 7 12C5.81593 12 4.69721 11.7481 3.71539 11.3008C2.71555 11.7572 1.62029 12 0.500001 12C0.315603 12 0.146173 11.8985 0.0591635 11.7359C-0.027846 11.5733 -0.0183098 11.3761 0.0839756 11.2226L0.687434 10.3175C0.890167 10.0134 1.05767 9.68744 1.18687 9.346C0.442008 8.39623 7.77784e-07 7.24553 7.77784e-07 6Z"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -122,6 +122,7 @@ export interface TabItem {
|
||||
icon?: Asset | AnySvelteComponent
|
||||
color?: string
|
||||
tooltip?: IntlString
|
||||
showNotify?: boolean
|
||||
action?: () => void
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
export let icon: Asset | AnySvelteComponent | ComponentType
|
||||
export let iconProps: any | undefined = undefined
|
||||
export let size: 'x-small' | 'small' | 'medium' | 'large' = 'medium'
|
||||
export let size: 'x-small' | 'small' = 'small'
|
||||
export let action: (ev: MouseEvent) => Promise<void> | void = async () => {}
|
||||
export let opened = false
|
||||
</script>
|
||||
@ -30,8 +30,13 @@
|
||||
|
||||
<style lang="scss">
|
||||
.action {
|
||||
padding: 0.125rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0.25rem;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color);
|
||||
|
@ -29,6 +29,6 @@
|
||||
|
||||
<ActivityMessageAction
|
||||
icon={view.icon.Pin}
|
||||
iconProps={object.isPinned ? { fill: '#3265cb' } : undefined}
|
||||
iconProps={{ fill: object.isPinned ? '#3265cb' : 'currentColor' }}
|
||||
action={toggleMessagePinning}
|
||||
/>
|
||||
|
@ -13,13 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { ActionIcon } from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { ActivityMessage, SavedMessage } from '@hcengineering/activity'
|
||||
import preference from '@hcengineering/preference'
|
||||
|
||||
import Bookmark from './icons/Bookmark.svelte'
|
||||
import BookmarkBorder from './icons/BookmarkBorder.svelte'
|
||||
import ActivityMessageAction from './ActivityMessageAction.svelte'
|
||||
import Bookmark from './icons/Bookmark.svelte'
|
||||
|
||||
export let object: ActivityMessage
|
||||
|
||||
@ -45,7 +45,8 @@
|
||||
</script>
|
||||
|
||||
<ActivityMessageAction
|
||||
icon={Bookmark}
|
||||
iconProps={savedMessage ? { fill: '#3265cb' } : undefined}
|
||||
icon={savedMessage ? Bookmark : BookmarkBorder}
|
||||
size={savedMessage ? 'x-small' : 'small'}
|
||||
iconProps={{ fill: savedMessage ? 'var(--global-accent-TextColor)' : 'currentColor' }}
|
||||
action={toggleSaveMessage}
|
||||
/>
|
||||
|
@ -21,7 +21,7 @@
|
||||
import { Person } from '@hcengineering/contact'
|
||||
import { Avatar, EmployeePresenter, SystemAvatar } from '@hcengineering/contact-resources'
|
||||
import core, { getDisplayTime } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Action, Label } from '@hcengineering/ui'
|
||||
import { getActions } from '@hcengineering/view-resources'
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
|
||||
import ActivityMessageActions from '../ActivityMessageActions.svelte'
|
||||
import { isReactionMessage } from '../../activityMessagesUtils'
|
||||
import Bookmark from '../icons/Bookmark.svelte'
|
||||
|
||||
export let message: DisplayActivityMessage
|
||||
export let parentMessage: DisplayActivityMessage | undefined = undefined
|
||||
@ -54,12 +55,20 @@
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const savedMessageQuery = createQuery()
|
||||
|
||||
let allActionIds: string[] = []
|
||||
|
||||
let element: HTMLDivElement | undefined = undefined
|
||||
let extensions: ActivityMessageExtension[] = []
|
||||
let isActionsOpened = false
|
||||
|
||||
let isSaved = false
|
||||
|
||||
savedMessageQuery.query(activity.class.SavedMessage, { attachedTo: message._id }, (res) => {
|
||||
isSaved = res.length > 0
|
||||
})
|
||||
|
||||
$: withActions &&
|
||||
getActions(client, message, activity.class.ActivityMessage).then((res) => {
|
||||
allActionIds = res.map(({ _id }) => _id)
|
||||
@ -119,7 +128,7 @@
|
||||
<div class="notify" />
|
||||
{/if}
|
||||
{#if !embedded}
|
||||
<div class="min-w-6 mt-1">
|
||||
<div class="min-w-6 mt-1 relative">
|
||||
{#if $$slots.icon}
|
||||
<slot name="icon" />
|
||||
{:else if person}
|
||||
@ -127,6 +136,11 @@
|
||||
{:else}
|
||||
<SystemAvatar size="medium" />
|
||||
{/if}
|
||||
{#if isSaved}
|
||||
<div class="saveMarker">
|
||||
<Bookmark size="xx-small" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="embeddedMarker" />
|
||||
@ -184,7 +198,7 @@
|
||||
<style lang="scss">
|
||||
@keyframes highlight {
|
||||
50% {
|
||||
background-color: var(--theme-warning-color);
|
||||
background-color: var(--global-ui-highlight-BackgroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,7 +218,7 @@
|
||||
}
|
||||
|
||||
&.highlighted {
|
||||
animation: highlight 5000ms ease-in-out;
|
||||
animation: highlight 3000ms ease-in-out;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@ -292,4 +306,21 @@
|
||||
border-radius: 0.5rem;
|
||||
background: var(--secondary-button-default);
|
||||
}
|
||||
|
||||
.saveMarker {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
padding: var(--spacing-1);
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(0deg, var(--button-primary-BackgroundColor), var(--button-primary-BackgroundColor)),
|
||||
linear-gradient(0deg, var(--global-ui-BackgroundColor), var(--global-ui-BackgroundColor));
|
||||
border: 1px solid var(--global-ui-BackgroundColor);
|
||||
top: -0.5rem;
|
||||
left: -0.5rem;
|
||||
color: var(--white-color);
|
||||
}
|
||||
</style>
|
||||
|
@ -13,12 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let size: 'tiny' | 'xx-small' | 'x-small' | 'small' | 'medium' | 'large' = 'small'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 10 14" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.2,18c-0.1,0-0.2,0-0.3-0.1l-6-4l-6,4c-0.2,0.1-0.4,0.1-0.5,0c-0.2-0.1-0.3-0.3-0.3-0.4V4.2 C3.2,3,4.3,2,5.5,2h8.9c1.3,0,2.3,1,2.3,2.2v13.3c0,0.2-0.1,0.4-0.3,0.4C16.4,18,16.3,18,16.2,18z M10,12.8c0.1,0,0.2,0,0.3,0.1 l5.5,3.6V4.2c0-0.6-0.6-1.2-1.3-1.2H5.5C4.8,3,4.2,3.5,4.2,4.2v12.4l5.5-3.6C9.8,12.9,9.9,12.8,10,12.8z"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0 2C0 0.895431 0.89543 0 2 0H8C9.10457 0 10 0.89543 10 2V13.5C10 13.6844 9.89851 13.8538 9.73593 13.9408C9.57335 14.0278 9.37608 14.0183 9.22265 13.916L5 11.1009L0.77735 13.916C0.623922 14.0183 0.42665 14.0278 0.264071 13.9408C0.101492 13.8538 0 13.6844 0 13.5V2Z"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -0,0 +1,24 @@
|
||||
<!--
|
||||
// Copyright © 2023 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
export let fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.2,18c-0.1,0-0.2,0-0.3-0.1l-6-4l-6,4c-0.2,0.1-0.4,0.1-0.5,0c-0.2-0.1-0.3-0.3-0.3-0.4V4.2 C3.2,3,4.3,2,5.5,2h8.9c1.3,0,2.3,1,2.3,2.2v13.3c0,0.2-0.1,0.4-0.3,0.4C16.4,18,16.3,18,16.2,18z M10,12.8c0.1,0,0.2,0,0.3,0.1 l5.5,3.6V4.2c0-0.6-0.6-1.2-1.3-1.2H5.5C4.8,3,4.2,3.5,4.2,4.2v12.4l5.5-3.6C9.8,12.9,9.9,12.8,10,12.8z"
|
||||
/>
|
||||
</svg>
|
@ -8,12 +8,16 @@
|
||||
<symbol id="lock" viewBox="0 0 16 16">
|
||||
<path d="M12,7.1h-0.7V5.4c0-1.8-1.5-3.3-3.3-3.3c-1.8,0-3.3,1.5-3.3,3.3v1.7H4c-0.8,0-1.5,0.7-1.5,1.5v4.1c0,0.8,0.7,1.5,1.5,1.5h8 c0.8,0,1.5-0.7,1.5-1.5V8.6C13.5,7.8,12.8,7.1,12,7.1z M5.7,5.4c0-1.2,1-2.3,2.3-2.3s2.3,1,2.3,2.3v1.7H5.7V5.4z M12.5,12.7 c0,0.3-0.2,0.5-0.5,0.5H4c-0.3,0-0.5-0.2-0.5-0.5V8.6c0-0.3,0.2-0.5,0.5-0.5h8c0.3,0,0.5,0.2,0.5,0.5V12.7z"/>
|
||||
</symbol>
|
||||
<symbol id="thread" viewBox="0 0 24 24">
|
||||
<path d="M22.8,10.2c0-5-4.3-9-9.6-9s-9.6,4-9.6,9c0,0.1,0,0.3,0,0.4c-1.5,1.1-2.4,2.8-2.4,4.6c0,1.7,0.8,3.3,2.2,4.4 l-0.2,1.5c-0.1,0.5,0.2,1,0.6,1.3c0.2,0.1,0.5,0.2,0.7,0.2c0.2,0,0.5-0.1,0.7-0.2l2.4-1.4c1.7-0.1,3.3-0.8,4.4-1.9 c0.3,0,0.6,0.1,0.8,0.1l4,2.4c0.3,0.2,0.6,0.3,0.9,0.3c0.3,0,0.7-0.1,0.9-0.3c0.6-0.4,0.9-1,0.8-1.7l-0.3-2.7 C21.5,15.5,22.8,13,22.8,10.2z M7.5,19.6c-0.1,0-0.3,0-0.4,0.1l-2.4,1.4l0.2-1.7c0-0.3-0.1-0.6-0.3-0.7c-1.2-0.8-1.9-2.1-1.9-3.5 c0-1.4,0.8-2.8,2-3.6c0.8-0.5,1.7-0.8,2.7-0.8c2.3,0,4.2,1.5,4.7,3.5c0.1,0.3,0.1,0.6,0.1,0.9c0,0.2,0,0.5-0.1,0.7c0,0,0,0.1,0,0.1 c-0.1,0.7-0.4,1.3-0.9,1.9C10.4,19,9,19.6,7.5,19.6z M18,16.2c-0.2,0.2-0.3,0.4-0.3,0.7l0.4,3.2c0,0.1-0.1,0.2-0.1,0.2 c-0.1,0-0.1,0.1-0.3,0l-4.2-2.5c-0.1-0.1-0.3-0.1-0.4-0.1c0.1-0.2,0.2-0.3,0.2-0.5c0,0,0,0,0-0.1c0.1-0.3,0.2-0.6,0.2-0.8 c0-0.1,0-0.1,0-0.2c0-0.3,0.1-0.6,0.1-0.9c0-0.2,0-0.4,0-0.6c-0.3-3-3-5.3-6.2-5.3c-0.3,0-0.6,0-0.8,0.1c-0.1,0-0.1,0-0.2,0 c-0.3,0-0.5,0.1-0.8,0.2c0,0,0,0-0.1,0C5.4,9.7,5.3,9.7,5.1,9.8c0.3-3.9,3.8-7,8.1-7c4.5,0,8.1,3.4,8.1,7.5 C21.2,12.6,20.1,14.8,18,16.2z" />
|
||||
<symbol id="thread" viewBox="0 0 14 12">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 1C3.60474 1 1 3.31362 1 6C1 7.08097 1.4127 8.08912 2.12929 8.91766C2.24292 9.04906 2.28022 9.23002 2.22779 9.39564C2.05513 9.9411 1.7993 10.4525 1.48219 10.9281C2.18374 10.8248 2.86228 10.6118 3.49342 10.2991C3.63342 10.2297 3.79779 10.2298 3.93773 10.2993C4.83161 10.7431 5.87792 11 7 11C10.3953 11 13 8.68638 13 6C13 3.31362 10.3953 1 7 1ZM7.77784e-07 6C7.77784e-07 2.61125 3.21556 0 7 0C10.7844 0 14 2.61125 14 6C14 9.38875 10.7844 12 7 12C5.81593 12 4.69721 11.7481 3.71539 11.3008C2.71555 11.7572 1.62029 12 0.500001 12C0.315603 12 0.146173 11.8985 0.0591635 11.7359C-0.027846 11.5733 -0.0183098 11.3761 0.0839756 11.2226L0.687434 10.3175C0.890167 10.0134 1.05767 9.68744 1.18687 9.346C0.442008 8.39623 7.77784e-07 7.24553 7.77784e-07 6Z"/>
|
||||
</symbol>
|
||||
<symbol id="channelbrowser" viewBox="0 0 24 24">
|
||||
<path d="M12.4,20.2H8.6c-4.3,0-5.9-1.6-5.9-5.8V8.7c0-4.4,1.5-6,5.9-6h5.7c4.4,0,6,1.6,6,6v3.7c0,0.4,0.3,0.7,0.8,0.7 s0.8-0.3,0.8-0.7V8.7c0-5.2-2.2-7.5-7.5-7.5H8.6c-5.2,0-7.4,2.2-7.4,7.5v5.7c0,5.1,2.3,7.3,7.4,7.3h3.8c0.4,0,0.7-0.3,0.7-0.8 S12.8,20.2,12.4,20.2z" />
|
||||
<path d="M22.2,21.4l-1-1c0,0,0,0-0.1,0c0.4-0.6,0.7-1.4,0.7-2.2c0-2.2-1.7-3.9-3.9-3.9s-4,1.7-4,3.9c0,2.1,1.8,4,4,4 c0.8,0,1.6-0.2,2.2-0.7c0,0,0,0,0,0.1l1,1c0.1,0.1,0.3,0.2,0.5,0.2s0.4-0.1,0.5-0.2C22.5,22.1,22.5,21.7,22.2,21.4z M17.9,20.5 c-1.3,0-2.5-1.2-2.5-2.5c0-1.4,1.1-2.4,2.5-2.4s2.4,1.1,2.4,2.4S19.3,20.5,17.9,20.5z" />
|
||||
<path d="M16.3,9.5c0.4,0,0.8-0.3,0.8-0.8S16.7,8,16.3,8h-2.9l0.3-2.9c0-0.4-0.3-0.8-0.7-0.8c-0.4,0-0.8,0.3-0.8,0.7 l-0.3,3H9.7L10,5.1c0-0.4-0.3-0.8-0.7-0.8C9,4.2,8.6,4.5,8.6,4.9L8.2,8H5.3C4.9,8,4.5,8.3,4.5,8.7s0.3,0.8,0.8,0.8h2.8l-0.2,2.1 H4.7c-0.4,0-0.8,0.3-0.8,0.8S4.3,13,4.7,13h3l-0.3,2.9c0,0.4,0.3,0.8,0.7,0.8c0,0,0.1,0,0.1,0c0.4,0,0.7-0.3,0.7-0.7l0.3-3h2.2 l-0.3,2.9c0,0.4,0.3,0.8,0.7,0.8c0,0,0.1,0,0.1,0c0.4,0,0.7-0.3,0.7-0.7l0.3-3h2.8c0.4,0,0.8-0.3,0.8-0.8s-0.3-0.8-0.8-0.8H13 l0.2-2.1H16.3z M11.5,11.5H9.3l0.2-2.1h2.2L11.5,11.5z" />
|
||||
</symbol>
|
||||
<symbol id="copy" viewBox="0 0 12 14">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 2C3 0.89543 3.89543 0 5 0H10C11.1046 0 12 0.895431 12 2V9C12 10.1046 11.1046 11 10 11H5C3.89543 11 3 10.1046 3 9V2ZM5 1C4.44772 1 4 1.44772 4 2V9C4 9.55228 4.44772 10 5 10H10C10.5523 10 11 9.55229 11 9V2C11 1.44772 10.5523 1 10 1H5Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4C1.44772 4 1 4.44772 1 5V12C1 12.5523 1.44772 13 2 13H7C7.55228 13 8 12.5523 8 12V11H9V12C9 13.1046 8.10457 14 7 14H2C0.895431 14 0 13.1046 0 12V5C0 3.89543 0.895431 3 2 3H3V4H2Z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.4 KiB |
@ -91,6 +91,9 @@
|
||||
"DescriptionOptional": "Description (optional)",
|
||||
"Visibility": "Visibility",
|
||||
"Public": "Public",
|
||||
"Private": "Private"
|
||||
"Private": "Private",
|
||||
"NewDirectChat": "New direct chat",
|
||||
"AddMembers": "Add members",
|
||||
"PinnedCount": "{count} pinned"
|
||||
}
|
||||
}
|
@ -91,6 +91,9 @@
|
||||
"DescriptionOptional": "Описание (необязательно)",
|
||||
"Visibility": "Видимость",
|
||||
"Public": "Публичный",
|
||||
"Private": "Закрытый"
|
||||
"Private": "Закрытый",
|
||||
"NewDirectChat": "Новый личный чат",
|
||||
"AddMembers": "Добавить участников",
|
||||
"PinnedCount": "{count} закреплено"
|
||||
}
|
||||
}
|
@ -22,5 +22,6 @@ loadMetadata(chunter.icon, {
|
||||
Hashtag: `${icons}#hashtag`,
|
||||
Thread: `${icons}#thread`,
|
||||
Lock: `${icons}#lock`,
|
||||
ChannelBrowser: `${icons}#channelbrowser`
|
||||
ChannelBrowser: `${icons}#channelbrowser`,
|
||||
Copy: `${icons}#copy`
|
||||
})
|
||||
|
@ -19,10 +19,12 @@
|
||||
import { Channel } from '@hcengineering/chunter'
|
||||
import { ActivityMessagesFilter } from '@hcengineering/activity'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
|
||||
import Header from './Header.svelte'
|
||||
import chunter from '../plugin'
|
||||
import { getChannelIcon, getChannelName } from '../utils'
|
||||
import PinnedMessages from './PinnedMessages.svelte'
|
||||
|
||||
export let _id: Ref<Doc>
|
||||
export let _class: Ref<Class<Doc>>
|
||||
@ -76,7 +78,9 @@
|
||||
{isAsideShown}
|
||||
on:aside-toggled
|
||||
on:close
|
||||
/>
|
||||
>
|
||||
<PinnedMessages {_id} {_class} />
|
||||
</Header>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
119
plugins/chunter-resources/src/components/ChannelMembers.svelte
Normal file
119
plugins/chunter-resources/src/components/ChannelMembers.svelte
Normal file
@ -0,0 +1,119 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { Person } from '@hcengineering/contact'
|
||||
import { ButtonIcon, IconDelete, ModernButton, Scroller } from '@hcengineering/ui'
|
||||
import { IconAddMember, personByIdStore, UserDetails } from '@hcengineering/contact-resources'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let ids: Ref<Person>[] = []
|
||||
export let disableRemoveFor: Ref<Person>[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let persons: Person[] = []
|
||||
|
||||
$: updatePersons(ids)
|
||||
|
||||
function updatePersons (ids: Ref<Person>[]) {
|
||||
persons = ids.map((_id) => $personByIdStore.get(_id)).filter((person): person is Person => !!person)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="item" style:padding="var(--spacing-1_5)" class:withoutBorder={persons.length === 0}>
|
||||
<ModernButton
|
||||
label={chunter.string.AddMembers}
|
||||
icon={IconAddMember}
|
||||
iconSize="small"
|
||||
kind="secondary"
|
||||
size="small"
|
||||
on:click={() => dispatch('add')}
|
||||
/>
|
||||
</div>
|
||||
<Scroller>
|
||||
{#each persons as person, index}
|
||||
<div class="item" class:withoutBorder={index === persons.length - 1}>
|
||||
<div class="item__content" class:disabled={disableRemoveFor.includes(person._id)}>
|
||||
<UserDetails {person} />
|
||||
{#if !disableRemoveFor.includes(person._id)}
|
||||
<div class="item__action">
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
size="small"
|
||||
on:click={() => {
|
||||
dispatch('remove', person._id)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Scroller>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1px;
|
||||
border-radius: 0.75rem;
|
||||
background: var(--global-ui-highlight-BackgroundColor);
|
||||
border: 1px solid var(--global-ui-BorderColor);
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: var(--spacing-0_75);
|
||||
border-bottom: 1px solid var(--global-ui-BorderColor);
|
||||
|
||||
&.withoutBorder {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.item__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-0_75);
|
||||
border-radius: var(--small-BorderRadius);
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--global-ui-highlight-BackgroundColor);
|
||||
|
||||
.item__action {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item__action {
|
||||
visibility: hidden;
|
||||
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -19,12 +19,13 @@
|
||||
import activity, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/activity'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { combineActivityMessages } from '@hcengineering/activity-resources'
|
||||
import { Channel } from '@hcengineering/chunter'
|
||||
|
||||
import Channel from './Channel.svelte'
|
||||
import PinnedMessages from './PinnedMessages.svelte'
|
||||
import ChannelComponent from './Channel.svelte'
|
||||
import ChannelHeader from './ChannelHeader.svelte'
|
||||
import DocChatPanel from './chat/DocChatPanel.svelte'
|
||||
import DocAside from './chat/DocAside.svelte'
|
||||
import chunter from '../plugin'
|
||||
import ChannelAside from './chat/ChannelAside.svelte'
|
||||
|
||||
export let context: DocNotifyContext
|
||||
export let object: Doc | undefined = undefined
|
||||
@ -47,7 +48,8 @@
|
||||
})
|
||||
|
||||
$: isDocChat = !hierarchy.isDerived(context.attachedToClass, chunter.class.ChunterSpace)
|
||||
$: withAside = !embedded && isDocChat && !isThreadOpened
|
||||
$: withAside =
|
||||
!embedded && !isThreadOpened && !hierarchy.isDerived(context.attachedToClass, chunter.class.DirectMessage)
|
||||
|
||||
$: updateMessagesQuery(isDocChat ? activity.class.ActivityMessage : chunter.class.ChatMessage, context.attachedTo)
|
||||
|
||||
@ -75,6 +77,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toChannel (object?: Doc) {
|
||||
return object as Channel | undefined
|
||||
}
|
||||
|
||||
function toChannelRef (ref: Ref<Doc>) {
|
||||
return ref as Ref<Channel>
|
||||
}
|
||||
|
||||
defineSeparators('aside', panelSeparators)
|
||||
</script>
|
||||
|
||||
@ -99,8 +109,7 @@
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="popupPanel-body__main">
|
||||
<PinnedMessages {context} />
|
||||
<Channel {context} {object} {filters} messages={activityMessages} />
|
||||
<ChannelComponent {context} {object} {filters} messages={activityMessages} />
|
||||
</div>
|
||||
|
||||
{#if withAside && isAsideShown}
|
||||
@ -108,7 +117,15 @@
|
||||
<div class="popupPanel-body__aside" class:float={false} class:shown={withAside && isAsideShown}>
|
||||
<Separator name="aside" float index={0} />
|
||||
<div class="antiPanel-wrap__content">
|
||||
<DocChatPanel _id={context.attachedTo} _class={context.attachedToClass} {object} />
|
||||
{#if hierarchy.isDerived(context.attachedToClass, chunter.class.Channel)}
|
||||
<ChannelAside
|
||||
_id={toChannelRef(context.attachedTo)}
|
||||
_class={context.attachedToClass}
|
||||
object={toChannel(object)}
|
||||
/>
|
||||
{:else}
|
||||
<DocAside _id={context.attachedTo} _class={context.attachedToClass} {object} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -73,8 +73,11 @@
|
||||
<div class="antiHSpacer x2" />
|
||||
{/if}
|
||||
{#if withFilters}
|
||||
<ChannelMessagesFilter bind:selectedFilters={filters} />
|
||||
<div class="mr-2">
|
||||
<ChannelMessagesFilter bind:selectedFilters={filters} />
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if titleKind === 'breadcrumbs'}
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
|
@ -13,15 +13,18 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { eventToHTMLElement, Label, showPopup } from '@hcengineering/ui'
|
||||
import { eventToHTMLElement, Label, ModernButton, showPopup } from '@hcengineering/ui'
|
||||
import PinnedMessagesPopup from './PinnedMessagesPopup.svelte'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let context: DocNotifyContext
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let _id: Ref<Doc>
|
||||
|
||||
const pinnedQuery = createQuery()
|
||||
|
||||
@ -29,28 +32,25 @@
|
||||
|
||||
$: pinnedQuery.query(
|
||||
activity.class.ActivityMessage,
|
||||
{ attachedTo: context.attachedTo, isPinned: true },
|
||||
{ attachedTo: _id, isPinned: true },
|
||||
(res: ActivityMessage[]) => {
|
||||
pinnedMessagesCount = res.length
|
||||
}
|
||||
)
|
||||
function openMessagesPopup (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) {
|
||||
showPopup(
|
||||
PinnedMessagesPopup,
|
||||
{ attachedTo: context.attachedTo, attachedToClass: context.attachedToClass },
|
||||
eventToHTMLElement(ev)
|
||||
)
|
||||
function openMessagesPopup (ev: MouseEvent) {
|
||||
showPopup(PinnedMessagesPopup, { attachedTo: _id, attachedToClass: _class }, eventToHTMLElement(ev))
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if pinnedMessagesCount > 0}
|
||||
<div class="bottom-divider over-underline pt-2 pb-2 container">
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div on:click={openMessagesPopup}>
|
||||
<Label label={chunter.string.Pinned} />
|
||||
{pinnedMessagesCount}
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<ModernButton
|
||||
icon={view.icon.Pin}
|
||||
size="small"
|
||||
label={chunter.string.PinnedCount}
|
||||
labelParams={{ count: pinnedMessagesCount }}
|
||||
on:click={openMessagesPopup}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -13,22 +13,31 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import { ActivityMessagePresenter } from '@hcengineering/activity-resources'
|
||||
import { ActionIcon, IconClose } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let attachedTo: Ref<Doc>
|
||||
export let attachedToClass: Ref<Class<Doc>>
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const messagesQuery = createQuery()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let pinnedMessages: DisplayActivityMessage[] = []
|
||||
|
||||
$: messagesQuery.query(activity.class.ActivityMessage, { attachedTo, isPinned: true }, (res: ActivityMessage[]) => {
|
||||
pinnedMessages = res as DisplayActivityMessage[]
|
||||
|
||||
if (pinnedMessages.length === 0) {
|
||||
dispatch('close')
|
||||
}
|
||||
})
|
||||
|
||||
async function unPinMessaage (message: ActivityMessage): Promise<void> {
|
||||
@ -42,11 +51,12 @@
|
||||
<ActivityMessagePresenter
|
||||
value={message}
|
||||
withActions={false}
|
||||
skipLabel={!hierarchy.isDerived(message._class, activity.class.DocUpdateMessage)}
|
||||
hoverable={false}
|
||||
skipLabel={!hierarchy.isDerived(attachedToClass, chunter.class.ChunterSpace)}
|
||||
/>
|
||||
<div class="remove">
|
||||
<div class="actions">
|
||||
<ActionIcon
|
||||
size="medium"
|
||||
size="small"
|
||||
icon={IconClose}
|
||||
action={() => {
|
||||
unPinMessaage(message)
|
||||
@ -58,20 +68,38 @@
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.message {
|
||||
min-width: 30rem;
|
||||
border-radius: var(--medium-BorderRadius);
|
||||
|
||||
.actions {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
right: 0.85rem;
|
||||
box-shadow: 0.25rem 0.75rem 1rem 0.125rem var(--global-popover-ShadowColor);
|
||||
border: 1px solid var(--global-subtle-ui-BorderColor);
|
||||
background: linear-gradient(0deg, var(--global-surface-01-BorderColor), var(--global-surface-01-BorderColor)),
|
||||
linear-gradient(0deg, var(--global-ui-BackgroundColor), var(--global-ui-BackgroundColor));
|
||||
padding: var(--spacing-0_5);
|
||||
border-radius: var(--small-BorderRadius);
|
||||
}
|
||||
|
||||
&:hover > .actions {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--global-ui-BackgroundColor);
|
||||
}
|
||||
}
|
||||
|
||||
.antiPopup {
|
||||
max-width: 40rem;
|
||||
}
|
||||
.message {
|
||||
min-width: 30rem;
|
||||
}
|
||||
.popup {
|
||||
padding: 1rem;
|
||||
max-height: 20rem;
|
||||
max-height: 24.5rem;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
.remove {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -0,0 +1,154 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 core, { Account, Class, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { Label, showPopup, tooltip } from '@hcengineering/ui'
|
||||
|
||||
import { Channel, ChunterSpace } from '@hcengineering/chunter'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { EmployeeBox, personAccountByIdStore, SelectUsersPopup } from '@hcengineering/contact-resources'
|
||||
|
||||
import ChannelMembers from '../ChannelMembers.svelte'
|
||||
import DocAside from './DocAside.svelte'
|
||||
import { joinChannel, leaveChannel } from '../../utils'
|
||||
|
||||
export let _id: Ref<ChunterSpace>
|
||||
export let _class: Ref<Class<ChunterSpace>>
|
||||
export let object: ChunterSpace | undefined
|
||||
|
||||
const currentAccount = getCurrentAccount()
|
||||
|
||||
let members: Ref<Person>[] = []
|
||||
|
||||
$: creatorPersonRef = object?.createdBy
|
||||
? $personAccountByIdStore.get(object.createdBy as Ref<PersonAccount>)?.person
|
||||
: undefined
|
||||
|
||||
$: disabledRemoveFor = currentAccount._id !== object?.createdBy && creatorPersonRef ? [creatorPersonRef] : []
|
||||
$: updateMembers(object)
|
||||
|
||||
function updateMembers (object: Channel | undefined) {
|
||||
if (object === undefined) {
|
||||
members = []
|
||||
return
|
||||
}
|
||||
|
||||
members = object.members
|
||||
.map((accountId) => {
|
||||
const personAccount = $personAccountByIdStore.get(accountId as Ref<PersonAccount>)
|
||||
if (personAccount === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return personAccount.person
|
||||
})
|
||||
.filter((_id): _id is Ref<Person> => !!_id)
|
||||
}
|
||||
|
||||
async function changeMembers (personRefs: Ref<Person>[], object?: Channel) {
|
||||
if (object === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const personAccounts = Array.from($personAccountByIdStore.values())
|
||||
|
||||
const newMembers: Ref<Account>[] = personAccounts
|
||||
.filter(({ person }) => personRefs.includes(person))
|
||||
.map(({ _id }) => _id)
|
||||
|
||||
const toLeave = object.members.filter((_id) => !newMembers.includes(_id))
|
||||
const toJoin = newMembers.filter((_id) => !object.members.includes(_id))
|
||||
|
||||
await Promise.all([leaveChannel(object, toLeave), joinChannel(object, toJoin)])
|
||||
}
|
||||
|
||||
async function removeMember (ev: CustomEvent) {
|
||||
if (object === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const personId = ev.detail as Ref<Person> | undefined
|
||||
|
||||
if (personId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const personAccount = Array.from($personAccountByIdStore.values()).find((account) => account.person === personId)
|
||||
|
||||
if (personAccount === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await leaveChannel(object, personAccount._id)
|
||||
}
|
||||
|
||||
function openSelectUsersPopup () {
|
||||
showPopup(
|
||||
SelectUsersPopup,
|
||||
{
|
||||
okLabel: presentation.string.Add,
|
||||
disableDeselectFor: disabledRemoveFor,
|
||||
selected: members
|
||||
},
|
||||
'top',
|
||||
(result?: Ref<Person>[]) => {
|
||||
if (result != null) {
|
||||
changeMembers(result, object)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<DocAside {_id} {_class} {object}>
|
||||
{#if object && creatorPersonRef}
|
||||
<div class="popupPanel-body__aside-grid" style:margin-top="0">
|
||||
<span
|
||||
class="labelOnPanel"
|
||||
use:tooltip={{
|
||||
component: Label,
|
||||
props: { label: core.string.CreatedBy }
|
||||
}}><Label label={core.string.CreatedBy} /></span
|
||||
>
|
||||
<div class="flex flex-grow min-w-0">
|
||||
<EmployeeBox
|
||||
value={creatorPersonRef}
|
||||
label={core.string.CreatedBy}
|
||||
kind={'link'}
|
||||
size={'medium'}
|
||||
avatarSize={'card'}
|
||||
width={'100%'}
|
||||
showNavigate={false}
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="members">
|
||||
<ChannelMembers
|
||||
ids={members}
|
||||
disableRemoveFor={disabledRemoveFor}
|
||||
on:add={openSelectUsersPopup}
|
||||
on:remove={removeMember}
|
||||
/>
|
||||
</div>
|
||||
</DocAside>
|
||||
|
||||
<style lang="scss">
|
||||
.members {
|
||||
padding: var(--spacing-2);
|
||||
}
|
||||
</style>
|
@ -125,7 +125,6 @@
|
||||
: 'landscape'} background-comp-header-color"
|
||||
>
|
||||
<div class="antiPanel-wrap__content">
|
||||
<NavHeader label={chunter.string.Chat} />
|
||||
<ChatNavigator
|
||||
{selectedContextId}
|
||||
selectedObjectClass={selectedContext?.attachedToClass}
|
||||
|
@ -41,20 +41,10 @@
|
||||
</script>
|
||||
|
||||
<Scroller>
|
||||
<div class="header">
|
||||
<div class="identifier">
|
||||
<Label label={clazz.label} />
|
||||
{#if objectLinkTitle}
|
||||
•
|
||||
<DocNavLink {object}>
|
||||
{objectLinkTitle}
|
||||
</DocNavLink>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if object}
|
||||
<DocAttributeBar {object} {mixins} ignoreKeys={objectChatPanel?.ignoreKeys ?? []} showHeader={false} />
|
||||
{/if}
|
||||
<slot />
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
@ -67,9 +67,9 @@
|
||||
hidden: false
|
||||
})
|
||||
|
||||
const navigate = await getResource(chunter.actionImpl.OpenChannel)
|
||||
const openChannelFn = await getResource(chunter.actionImpl.OpenChannel)
|
||||
|
||||
await navigate(undefined, undefined, { _id: notifyContextId })
|
||||
await openChannelFn(undefined, undefined, { _id: notifyContextId, mode: 'channels' })
|
||||
}
|
||||
|
||||
function handleCancel () {
|
||||
|
@ -13,24 +13,42 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
import { DirectMessage } from '@hcengineering/chunter'
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import contact, { Employee, PersonAccount } from '@hcengineering/contact'
|
||||
import core, { getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { getClient, SpaceCreateCard } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { UserBoxList } from '@hcengineering/contact-resources'
|
||||
import { SelectUsersPopup } from '@hcengineering/contact-resources'
|
||||
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Modal, showPopup } from '@hcengineering/ui'
|
||||
|
||||
import chunter from '../../../plugin'
|
||||
import { buildDmName } from '../../../utils'
|
||||
import ChannelMembers from '../../ChannelMembers.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const myAccId = getCurrentAccount()._id
|
||||
const query = createQuery()
|
||||
|
||||
let employeeIds: Ref<Employee>[] = []
|
||||
let dmName = ''
|
||||
let accounts: PersonAccount[] = []
|
||||
let hidden = true
|
||||
|
||||
$: loadDmName(accounts).then((r) => {
|
||||
dmName = r
|
||||
})
|
||||
$: query.query(contact.class.PersonAccount, { person: { $in: employeeIds } }, (res) => {
|
||||
accounts = res
|
||||
})
|
||||
|
||||
async function loadDmName (employeeAccounts: PersonAccount[]) {
|
||||
return await buildDmName(client, employeeAccounts)
|
||||
}
|
||||
|
||||
async function createDirectMessage () {
|
||||
const employeeAccounts = await client.findAll(contact.class.PersonAccount, { person: { $in: employeeIds } })
|
||||
@ -73,17 +91,74 @@
|
||||
hidden: false
|
||||
})
|
||||
|
||||
await navigate(undefined, undefined, { _id: notifyContextId })
|
||||
await navigate(undefined, undefined, { _id: notifyContextId, mode: 'direct' })
|
||||
}
|
||||
|
||||
function handleCancel () {
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
openSelectUsersPopup(true)
|
||||
})
|
||||
|
||||
function addMembersClicked () {
|
||||
openSelectUsersPopup(false)
|
||||
}
|
||||
|
||||
function openSelectUsersPopup (closeOnClose: boolean) {
|
||||
showPopup(
|
||||
SelectUsersPopup,
|
||||
{
|
||||
okLabel: presentation.string.Next,
|
||||
skipCurrentAccount: true,
|
||||
selected: employeeIds
|
||||
},
|
||||
'top',
|
||||
(result?: Ref<Employee>[]) => {
|
||||
if (result != null) {
|
||||
employeeIds = result
|
||||
hidden = false
|
||||
} else if (closeOnClose) {
|
||||
dispatch('close')
|
||||
}
|
||||
}
|
||||
)
|
||||
hidden = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<SpaceCreateCard
|
||||
label={chunter.string.NewDirectMessage}
|
||||
<Modal
|
||||
label={chunter.string.NewDirectChat}
|
||||
type="type-popup"
|
||||
{hidden}
|
||||
okLabel={presentation.string.Create}
|
||||
okAction={createDirectMessage}
|
||||
canSave={employeeIds.length > 0}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
onCancel={handleCancel}
|
||||
on:close
|
||||
>
|
||||
<UserBoxList label={chunter.string.Members} on:update={(evt) => (employeeIds = evt.detail)} />
|
||||
</SpaceCreateCard>
|
||||
<div class="hulyModal-content__titleGroup" style="padding: 0">
|
||||
<div class="title overflow-label mb-4" title={dmName}>
|
||||
{dmName}
|
||||
</div>
|
||||
|
||||
<ChannelMembers
|
||||
ids={employeeIds}
|
||||
on:add={addMembersClicked}
|
||||
on:remove={(ev) => {
|
||||
employeeIds = employeeIds.filter((id) => id !== ev.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
padding: var(--spacing-1);
|
||||
max-width: 40rem;
|
||||
color: var(--global-primary-TextColor);
|
||||
}
|
||||
</style>
|
@ -72,25 +72,6 @@
|
||||
}
|
||||
)
|
||||
|
||||
function getGroupActions (): Action[] {
|
||||
const result: Action[] = []
|
||||
|
||||
if (model.addLabel !== undefined && model.addComponent !== undefined) {
|
||||
result.push({
|
||||
label: model.addLabel,
|
||||
icon: IconAdd,
|
||||
action: async (_id: Ref<Doc>): Promise<void> => {
|
||||
dispatch('open')
|
||||
if (model.addComponent !== undefined) {
|
||||
showPopup(model.addComponent, {}, 'top')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function getPinnedActions (): Action[] {
|
||||
return [
|
||||
{
|
||||
@ -148,7 +129,7 @@
|
||||
{/if}
|
||||
|
||||
<div class="block">
|
||||
<ChatGroupHeader header={model.label} actions={getGroupActions()} />
|
||||
<ChatGroupHeader header={model.label} />
|
||||
{#each contexts as context (context._id)}
|
||||
<ChatNavItem {context} isSelected={selectedContextId === context._id} on:select />
|
||||
{/each}
|
||||
|
@ -21,15 +21,22 @@
|
||||
navigate,
|
||||
location as locationStore,
|
||||
Scroller,
|
||||
SearchEdit
|
||||
SearchEdit,
|
||||
Label,
|
||||
Button,
|
||||
IconAdd,
|
||||
showPopup,
|
||||
Menu,
|
||||
Action
|
||||
} from '@hcengineering/ui'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
|
||||
import { SpecialNavModel } from '@hcengineering/workbench'
|
||||
import { NavLink } from '@hcengineering/view-resources'
|
||||
import { TreeSeparator } from '@hcengineering/workbench-resources'
|
||||
import { getResource, type IntlString } from '@hcengineering/platform'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { getNotificationsCount, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
import chunter from '../../../plugin'
|
||||
import ChatNavGroup from './ChatNavGroup.svelte'
|
||||
@ -47,6 +54,26 @@
|
||||
const hierarchy = client.getHierarchy()
|
||||
const notificationClient = InboxNotificationsClientImpl.getClient()
|
||||
const contextsStore = notificationClient.docNotifyContexts
|
||||
const notificationsByContextStore = notificationClient.inboxNotificationsByContext
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: chunter.string.NewChannel,
|
||||
icon: chunter.icon.Hashtag,
|
||||
action: async (_id: Ref<Doc>): Promise<void> => {
|
||||
showPopup(chunter.component.CreateChannel, {}, 'top')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: chunter.string.NewDirectChat,
|
||||
icon: chunter.icon.Thread,
|
||||
action: async (_id: Ref<Doc>): Promise<void> => {
|
||||
showPopup(chunter.component.CreateDirectChat, {}, 'top')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const searchValue: string = ''
|
||||
|
||||
const modesConfig: Array<[Mode, IntlString, object]> = chatNavGroupsModel.map(({ id, tabLabel }) => [
|
||||
id,
|
||||
@ -56,8 +83,7 @@
|
||||
|
||||
let modeSelectorProps: IModeSelector
|
||||
let mode: Mode | undefined
|
||||
|
||||
const searchValue: string = ''
|
||||
let notifyModes: Mode[] = []
|
||||
|
||||
$: mode = ($locationStore.query?.mode ?? undefined) as Mode | undefined
|
||||
|
||||
@ -73,6 +99,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: getModesWithNotifications($contextsStore, $notificationsByContextStore).then((res) => {
|
||||
notifyModes = res
|
||||
})
|
||||
|
||||
$: updateSelectedContextMode(selectedObjectClass)
|
||||
|
||||
$: model = chatNavGroupsModel.find(({ id }) => id === mode) ?? chatNavGroupsModel[0]
|
||||
@ -115,9 +145,63 @@
|
||||
|
||||
return await getIsVisible(docNotifyContexts as any)
|
||||
}
|
||||
|
||||
async function addButtonClicked (ev: MouseEvent) {
|
||||
showPopup(Menu, { actions }, ev.target as HTMLElement)
|
||||
}
|
||||
|
||||
async function getModesWithNotifications (
|
||||
contexts: DocNotifyContext[],
|
||||
inboxNotificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>
|
||||
) {
|
||||
const contextIds = Array.from(inboxNotificationsByContext.keys())
|
||||
const modes: Mode[] = []
|
||||
|
||||
for (const contextId of contextIds) {
|
||||
if (modes.length === 3) {
|
||||
break
|
||||
}
|
||||
|
||||
const context = contexts.find(({ _id }) => _id === contextId)
|
||||
|
||||
if (
|
||||
context === undefined ||
|
||||
hierarchy.classHierarchyMixin(context.attachedToClass, activity.mixin.ActivityDoc) === undefined
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
let tmpMode: Mode = 'activity'
|
||||
|
||||
if (hierarchy.isDerived(context.attachedToClass, chunter.class.Channel)) {
|
||||
tmpMode = 'channels'
|
||||
} else if (hierarchy.isDerived(context.attachedToClass, chunter.class.DirectMessage)) {
|
||||
tmpMode = 'direct'
|
||||
}
|
||||
|
||||
if (modes.includes(tmpMode)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const notificationsCount = await getNotificationsCount(context, inboxNotificationsByContext.get(contextId))
|
||||
|
||||
if (notificationsCount > 0) {
|
||||
modes.push(tmpMode)
|
||||
}
|
||||
}
|
||||
|
||||
return modes
|
||||
}
|
||||
</script>
|
||||
|
||||
<Scroller shrink>
|
||||
<div class="header">
|
||||
<div class="overflow-label">
|
||||
<Label label={chunter.string.Chat} />
|
||||
</div>
|
||||
<Button icon={IconAdd} kind="primary" size="medium" iconProps={{ size: 'small' }} on:click={addButtonClicked} />
|
||||
</div>
|
||||
|
||||
{#each chatSpecials as special, row}
|
||||
{#if row > 0 && chatSpecials[row].position !== chatSpecials[row - 1].position}
|
||||
<TreeSeparator line />
|
||||
@ -145,12 +229,28 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ModeSelector props={modeSelectorProps} kind="separated-free" padding="0" expansion="stretch" />
|
||||
<ModeSelector
|
||||
props={modeSelectorProps}
|
||||
kind="separated-free"
|
||||
padding="0"
|
||||
expansion="stretch"
|
||||
notifyFor={notifyModes}
|
||||
/>
|
||||
<ChatNavGroup {selectedContextId} {model} on:select />
|
||||
<div class="antiNav-space" />
|
||||
</Scroller>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0.75rem;
|
||||
margin-left: 1.25rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
.search {
|
||||
padding: 12px;
|
||||
}
|
||||
|
@ -187,9 +187,11 @@
|
||||
color: var(--global-primary-TextColor);
|
||||
|
||||
&.withBackground {
|
||||
background-color: var(--global-ui-highlight-BackgroundColor);
|
||||
background: linear-gradient(0deg, var(--global-subtle-ui-BorderColor), var(--global-subtle-ui-BorderColor)),
|
||||
linear-gradient(0deg, var(--global-ui-BackgroundColor), var(--global-ui-BackgroundColor));
|
||||
padding: 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid var(--global-subtle-ui-BorderColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,10 +38,15 @@
|
||||
} from '@hcengineering/ui'
|
||||
import { FilterBar, FilterButton, SpacePresenter } from '@hcengineering/view-resources'
|
||||
import workbench from '@hcengineering/workbench'
|
||||
import { Channel } from '@hcengineering/chunter'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { get } from 'svelte/store'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
import { getChannelIcon } from '../../../utils'
|
||||
import { getChannelIcon, joinChannel, leaveChannel } from '../../../utils'
|
||||
import chunter from './../../../plugin'
|
||||
|
||||
export let _class: Ref<Class<Space>>
|
||||
export let _class: Ref<Class<Channel>> = chunter.class.Channel
|
||||
export let label: IntlString
|
||||
export let createItemDialog: AnyComponent | undefined = undefined
|
||||
export let createItemLabel: IntlString = presentation.string.Create
|
||||
@ -49,32 +54,37 @@
|
||||
export let withFilterButton: boolean = true
|
||||
export let search: string = ''
|
||||
|
||||
const me = getCurrentAccount()._id
|
||||
const client = getClient()
|
||||
const spaceQuery = createQuery()
|
||||
const me = getCurrentAccount()._id
|
||||
const channelsQuery = createQuery()
|
||||
|
||||
const notificationClient = InboxNotificationsClientImpl.getClient()
|
||||
|
||||
const sort: SortingQuery<Space> = {
|
||||
name: SortingOrder.Ascending
|
||||
}
|
||||
let searchQuery: DocumentQuery<Space>
|
||||
let resultQuery: DocumentQuery<Space>
|
||||
|
||||
let spaces: Space[] = []
|
||||
let searchQuery: DocumentQuery<Channel>
|
||||
let resultQuery: DocumentQuery<Channel>
|
||||
|
||||
let channels: Channel[] = []
|
||||
|
||||
$: updateSearchQuery(search)
|
||||
$: update(sort, resultQuery)
|
||||
|
||||
async function update (sort: SortingQuery<Space>, resultQuery: DocumentQuery<Space>): Promise<void> {
|
||||
const options: FindOptions<Space> = {
|
||||
async function update (sort: SortingQuery<Channel>, resultQuery: DocumentQuery<Channel>): Promise<void> {
|
||||
const options: FindOptions<Channel> = {
|
||||
sort
|
||||
}
|
||||
|
||||
spaceQuery.query(
|
||||
channelsQuery.query(
|
||||
_class,
|
||||
{
|
||||
...resultQuery
|
||||
...resultQuery,
|
||||
private: false
|
||||
},
|
||||
(res) => {
|
||||
spaces = res
|
||||
channels = res
|
||||
},
|
||||
options
|
||||
)
|
||||
@ -88,27 +98,43 @@
|
||||
showPopup(createItemDialog as AnyComponent, {}, 'middle')
|
||||
}
|
||||
|
||||
async function join (space: Space): Promise<void> {
|
||||
if (space.members.includes(me)) return
|
||||
await client.update(space, {
|
||||
$push: {
|
||||
members: me
|
||||
}
|
||||
})
|
||||
async function join (channel: Channel): Promise<void> {
|
||||
if (channel.members.includes(me)) {
|
||||
return
|
||||
}
|
||||
|
||||
await joinChannel(channel, me)
|
||||
}
|
||||
|
||||
async function leave (space: Space): Promise<void> {
|
||||
if (!space.members.includes(me)) return
|
||||
await client.update(space, {
|
||||
$pull: {
|
||||
members: me
|
||||
}
|
||||
})
|
||||
async function leave (channel: Channel): Promise<void> {
|
||||
if (!channel.members.includes(me)) {
|
||||
return
|
||||
}
|
||||
await leaveChannel(channel, me)
|
||||
}
|
||||
|
||||
async function view (space: Space): Promise<void> {
|
||||
async function view (channel: Channel): Promise<void> {
|
||||
const loc = getCurrentResolvedLocation()
|
||||
loc.path[3] = space._id
|
||||
const context = get(notificationClient.docNotifyContextByDoc).get(channel._id)
|
||||
|
||||
let contextId = context?._id
|
||||
|
||||
if (contextId === undefined) {
|
||||
contextId = await client.createDoc(notification.class.DocNotifyContext, channel.space, {
|
||||
attachedToClass: channel._class,
|
||||
attachedTo: channel._id,
|
||||
user: me,
|
||||
hidden: false,
|
||||
lastViewedTimestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
if (contextId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
loc.path[3] = contextId
|
||||
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
@ -154,12 +180,13 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<FilterBar {_class} query={searchQuery} space={undefined} on:change={(e) => (resultQuery = e.detail)} />
|
||||
<Scroller padding={'2.5rem'}>
|
||||
<div class="spaces-container">
|
||||
{#each spaces as space (space._id)}
|
||||
{@const icon = getChannelIcon(space._class)}
|
||||
{@const joined = space.members.includes(me)}
|
||||
{#each channels as channel (channel._id)}
|
||||
{@const icon = getChannelIcon(channel._class)}
|
||||
{@const joined = channel.members.includes(me)}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="item flex-between" tabindex="0">
|
||||
<div class="flex-col clear-mins">
|
||||
@ -167,16 +194,16 @@
|
||||
{#if icon}
|
||||
<div class="icon"><Icon {icon} size={'small'} /></div>
|
||||
{/if}
|
||||
<SpacePresenter value={space} />
|
||||
<SpacePresenter value={channel} />
|
||||
</div>
|
||||
<div class="flex-row-center">
|
||||
{#if joined}
|
||||
<Label label={workbench.string.Joined} />
|
||||
·
|
||||
{/if}
|
||||
{space.members.length}
|
||||
{channel.members.length}
|
||||
·
|
||||
{space.description}
|
||||
{channel.description}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tools flex-row-center gap-2">
|
||||
@ -185,7 +212,7 @@
|
||||
size={'x-large'}
|
||||
label={workbench.string.Leave}
|
||||
on:click={async () => {
|
||||
await leave(space)
|
||||
await leave(channel)
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
@ -193,7 +220,7 @@
|
||||
size={'x-large'}
|
||||
label={workbench.string.View}
|
||||
on:click={async () => {
|
||||
await view(space)
|
||||
await view(channel)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@ -201,7 +228,7 @@
|
||||
kind={'primary'}
|
||||
label={workbench.string.Join}
|
||||
on:click={async () => {
|
||||
await join(space)
|
||||
await join(channel)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -28,6 +28,7 @@
|
||||
import { SearchType } from '../../../utils'
|
||||
import plugin from '../../../plugin'
|
||||
import Header from '../../Header.svelte'
|
||||
import ChannelBrowser from './ChannelBrowser.svelte'
|
||||
|
||||
let userSearch_: string = ''
|
||||
userSearch.subscribe((v) => (userSearch_ = v))
|
||||
@ -43,7 +44,7 @@
|
||||
{ searchType: SearchType.Messages, component: MessagesBrowser },
|
||||
{
|
||||
searchType: SearchType.Channels,
|
||||
component: SpaceBrowser,
|
||||
component: ChannelBrowser,
|
||||
filterClass: plugin.class.Channel,
|
||||
props: {
|
||||
_class: plugin.class.Channel,
|
||||
|
@ -15,7 +15,6 @@
|
||||
import { type IntlString } from '@hcengineering/platform'
|
||||
import { type DocumentQuery } from '@hcengineering/core'
|
||||
import { type DocNotifyContext } from '@hcengineering/notification'
|
||||
import { type AnyComponent } from '@hcengineering/ui'
|
||||
|
||||
export type Mode = 'channels' | 'direct' | 'activity'
|
||||
|
||||
@ -23,7 +22,5 @@ export interface ChatNavGroupModel {
|
||||
id: Mode
|
||||
label: IntlString
|
||||
tabLabel: IntlString
|
||||
addLabel?: IntlString
|
||||
addComponent?: AnyComponent
|
||||
query: DocumentQuery<DocNotifyContext>
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export const chatSpecials: SpecialNavModel[] = [
|
||||
{
|
||||
id: 'chunterBrowser',
|
||||
label: chunter.string.ChunterBrowser,
|
||||
icon: workbench.icon.Search,
|
||||
icon: view.icon.Database,
|
||||
component: chunter.component.ChunterBrowser,
|
||||
position: 'top'
|
||||
},
|
||||
@ -80,9 +80,7 @@ export const chatNavGroupsModel: ChatNavGroupModel[] = [
|
||||
label: chunter.string.AllChannels,
|
||||
query: {
|
||||
attachedToClass: { $in: [chunter.class.Channel] }
|
||||
},
|
||||
addLabel: chunter.string.CreateChannel,
|
||||
addComponent: chunter.component.CreateChannel
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'direct',
|
||||
@ -90,9 +88,7 @@ export const chatNavGroupsModel: ChatNavGroupModel[] = [
|
||||
label: chunter.string.AllContacts,
|
||||
query: {
|
||||
attachedToClass: { $in: [chunter.class.DirectMessage] }
|
||||
},
|
||||
addLabel: chunter.string.NewDirectMessage,
|
||||
addComponent: chunter.component.CreateDirectMessage
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'activity',
|
||||
|
@ -41,7 +41,7 @@ import ChannelPanel from './components/ChannelPanel.svelte'
|
||||
import ChunterBrowser from './components/chat/specials/ChunterBrowser.svelte'
|
||||
import ConvertDmToPrivateChannelModal from './components/ConvertDmToPrivateChannel.svelte'
|
||||
import CreateChannel from './components/chat/create/CreateChannel.svelte'
|
||||
import CreateDirectMessage from './components/chat/create/CreateDirectMessage.svelte'
|
||||
import CreateDirectChat from './components/chat/create/CreateDirectChat.svelte'
|
||||
import DirectMessagePresenter from './components/DirectMessagePresenter.svelte'
|
||||
import DmHeader from './components/DmHeader.svelte'
|
||||
import DmPresenter from './components/DmPresenter.svelte'
|
||||
@ -85,6 +85,7 @@ import {
|
||||
navigateToThread
|
||||
} from './utils'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { type Mode } from './components/chat/types'
|
||||
|
||||
export { default as ChatMessagesPresenter } from './components/chat-message/ChatMessagesPresenter.svelte'
|
||||
export { default as ChatMessagePopup } from './components/chat-message/ChatMessagePopup.svelte'
|
||||
@ -144,19 +145,30 @@ async function ConvertDmToPrivateChannel (dm: DirectMessage): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
async function OpenChannel (notifyContext?: DocNotifyContext, evt?: Event): Promise<void> {
|
||||
async function OpenChannel (
|
||||
notifyContext?: DocNotifyContext,
|
||||
evt?: Event,
|
||||
props?: { mode?: Mode, _id: Ref<DocNotifyContext> }
|
||||
): Promise<void> {
|
||||
evt?.preventDefault()
|
||||
|
||||
closePanel()
|
||||
|
||||
const loc = getCurrentLocation()
|
||||
const id = notifyContext?._id
|
||||
const id = notifyContext?._id ?? props?._id
|
||||
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (loc.path[3] === id) {
|
||||
return
|
||||
}
|
||||
|
||||
loc.path[3] = id
|
||||
loc.path.length = 4
|
||||
loc.query = { mode: props?.mode ?? loc.query?.mode ?? null, message: null }
|
||||
|
||||
loc.fragment = undefined
|
||||
|
||||
navigate(loc)
|
||||
@ -242,7 +254,7 @@ export default async (): Promise<Resources> => ({
|
||||
},
|
||||
component: {
|
||||
CreateChannel,
|
||||
CreateDirectMessage,
|
||||
CreateDirectChat,
|
||||
ThreadParentPresenter,
|
||||
ThreadViewPanel,
|
||||
ChannelHeader,
|
||||
|
@ -24,7 +24,7 @@ import { type DocNotifyContext, type InboxNotification } from '@hcengineering/no
|
||||
export default mergeIds(chunterId, chunter, {
|
||||
component: {
|
||||
CreateChannel: '' as AnyComponent,
|
||||
CreateDirectMessage: '' as AnyComponent,
|
||||
CreateDirectChat: '' as AnyComponent,
|
||||
ChannelHeader: '' as AnyComponent,
|
||||
ChannelPanel: '' as AnyComponent,
|
||||
ThreadViewPanel: '' as AnyComponent,
|
||||
@ -103,6 +103,7 @@ export default mergeIds(chunterId, chunter, {
|
||||
NoMessages: '' as IntlString,
|
||||
On: '' as IntlString,
|
||||
Mentioned: '' as IntlString,
|
||||
SentMessage: '' as IntlString
|
||||
SentMessage: '' as IntlString,
|
||||
PinnedCount: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
@ -30,7 +30,8 @@ import {
|
||||
type Ref,
|
||||
type Space,
|
||||
type Class,
|
||||
type Timestamp
|
||||
type Timestamp,
|
||||
type Account
|
||||
} from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
@ -65,6 +66,14 @@ export async function getDmName (client: Client, space?: Space): Promise<string>
|
||||
|
||||
const employeeAccounts: PersonAccount[] = await getDmAccounts(client, space)
|
||||
|
||||
return await buildDmName(client, employeeAccounts)
|
||||
}
|
||||
|
||||
export async function buildDmName (client: Client, employeeAccounts: PersonAccount[]): Promise<string> {
|
||||
if (employeeAccounts.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let unsub: Unsubscriber | undefined
|
||||
const promise = new Promise<IdMap<Employee>>((resolve) => {
|
||||
unsub = employeeByIdStore.subscribe((p) => {
|
||||
@ -391,3 +400,27 @@ export function navigateToThread (loc: Location, contextId: Ref<DocNotifyContext
|
||||
loc.query = { message: _id }
|
||||
navigate(loc)
|
||||
}
|
||||
|
||||
export async function joinChannel (channel: Channel, value: Ref<Account> | Array<Ref<Account>>): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
await client.update(channel, { $push: { members: { $each: value, $position: 0 } } })
|
||||
}
|
||||
} else {
|
||||
await client.update(channel, { $push: { members: value } })
|
||||
}
|
||||
}
|
||||
|
||||
export async function leaveChannel (channel: Channel, value: Ref<Account> | Array<Ref<Account>>): Promise<void> {
|
||||
const client = getClient()
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
await client.update(channel, { $pull: { members: { $in: value } } })
|
||||
}
|
||||
} else {
|
||||
await client.update(channel, { $pull: { members: value } })
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +165,8 @@ export default plugin(chunterId, {
|
||||
Hashtag: '' as Asset,
|
||||
Thread: '' as Asset,
|
||||
Lock: '' as Asset,
|
||||
ChannelBrowser: '' as Asset
|
||||
ChannelBrowser: '' as Asset,
|
||||
Copy: '' as Asset
|
||||
},
|
||||
component: {
|
||||
DmHeader: '' as AnyComponent,
|
||||
@ -230,7 +231,9 @@ export default plugin(chunterId, {
|
||||
DescriptionOptional: '' as IntlString,
|
||||
Visibility: '' as IntlString,
|
||||
Public: '' as IntlString,
|
||||
Private: '' as IntlString
|
||||
Private: '' as IntlString,
|
||||
NewDirectChat: '' as IntlString,
|
||||
AddMembers: '' as IntlString
|
||||
},
|
||||
ids: {
|
||||
DMNotification: '' as Ref<NotificationType>,
|
||||
|
@ -101,6 +101,7 @@
|
||||
"HasNewMessagesIn": "has new messages in",
|
||||
"Employees": "Employees",
|
||||
"People": "People",
|
||||
"For": "For"
|
||||
"For": "For",
|
||||
"SelectUsers": "Select users"
|
||||
}
|
||||
}
|
||||
|
@ -101,6 +101,7 @@
|
||||
"HasNewMessagesIn": "имеет новые сообщения в",
|
||||
"Employees": "Сотрудники",
|
||||
"People": "Люди",
|
||||
"For": "Для"
|
||||
"For": "Для",
|
||||
"SelectUsers": "Выберите пользователей"
|
||||
}
|
||||
}
|
||||
|
115
plugins/contact-resources/src/components/SelectUsersPopup.svelte
Normal file
115
plugins/contact-resources/src/components/SelectUsersPopup.svelte
Normal file
@ -0,0 +1,115 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { createEventDispatcher } from 'svelte'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { deviceOptionsStore, EditWithIcon, IconSearch, Modal, Scroller } from '@hcengineering/ui'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Class, Ref } from '@hcengineering/core'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
|
||||
import contact from '../plugin'
|
||||
import UsersList from './UsersList.svelte'
|
||||
|
||||
export let _class: Ref<Class<Employee>> = contact.mixin.Employee
|
||||
export let searchField: string = 'name'
|
||||
export let searchMode: 'field' | 'fulltext' | 'disabled' = 'field'
|
||||
export let groupBy = '_class'
|
||||
export let okLabel: IntlString = presentation.string.Ok
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let selected: Ref<Employee>[] = []
|
||||
export let skipCurrentAccount = false
|
||||
export let disableDeselectFor: Ref<Employee>[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let search: string = ''
|
||||
let selectedIds: Ref<Employee>[] = selected
|
||||
|
||||
function handleCancel () {
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
function okAction () {
|
||||
dispatch('close', selectedIds)
|
||||
}
|
||||
|
||||
function handleSelectionChanged (event: CustomEvent) {
|
||||
selectedIds = event.detail ?? []
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
label={contact.string.SelectUsers}
|
||||
type="type-popup"
|
||||
padding="0"
|
||||
{okLabel}
|
||||
{okAction}
|
||||
canSave={selectedIds.length > 0}
|
||||
onCancel={handleCancel}
|
||||
on:close
|
||||
>
|
||||
<div class="hulyModal-content__titleGroup">
|
||||
<div class="search">
|
||||
<EditWithIcon
|
||||
icon={IconSearch}
|
||||
size="large"
|
||||
width="100%"
|
||||
autoFocus={!$deviceOptionsStore.isMobile}
|
||||
bind:value={search}
|
||||
on:change={() => dispatch('search', search)}
|
||||
on:input={() => dispatch('search', search)}
|
||||
{placeholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="line" />
|
||||
|
||||
<div class="users">
|
||||
<Scroller padding="0.75rem 0">
|
||||
<UsersList
|
||||
{_class}
|
||||
{searchField}
|
||||
{searchMode}
|
||||
{search}
|
||||
{groupBy}
|
||||
selected={selectedIds}
|
||||
{disableDeselectFor}
|
||||
{skipCurrentAccount}
|
||||
on:select={handleSelectionChanged}
|
||||
/>
|
||||
</Scroller>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--global-subtle-ui-BorderColor);
|
||||
}
|
||||
|
||||
.search {
|
||||
padding: 1.25rem;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.users {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 32rem;
|
||||
}
|
||||
</style>
|
43
plugins/contact-resources/src/components/UserDetails.svelte
Normal file
43
plugins/contact-resources/src/components/UserDetails.svelte
Normal file
@ -0,0 +1,43 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { getClient } from '@hcengineering/presentation'
|
||||
import { IconSize } from '@hcengineering/ui'
|
||||
import { Person, getName } from '@hcengineering/contact'
|
||||
|
||||
import Avatar from './Avatar.svelte'
|
||||
|
||||
export let person: Person
|
||||
export let avatarSize: IconSize = 'x-small'
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex-row-center" on:click>
|
||||
<Avatar avatar={person.avatar} size={avatarSize} name={person.name} on:accent-color />
|
||||
<div class="flex-col min-w-0 {avatarSize === 'tiny' || avatarSize === 'inline' ? 'ml-1' : 'ml-3'}">
|
||||
<div class="label overflow-label text-left">{getName(hierarchy, person)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label {
|
||||
color: var(--global-primary-TextColor);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
141
plugins/contact-resources/src/components/UsersList.svelte
Normal file
141
plugins/contact-resources/src/components/UsersList.svelte
Normal file
@ -0,0 +1,141 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 contact, { Employee, PersonAccount } from '@hcengineering/contact'
|
||||
import { Class, flipSet, getCurrentAccount, getObjectValue, Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { CheckBox, createFocusManager, FocusHandler, ListView } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import UserDetails from './UserDetails.svelte'
|
||||
import { personByIdStore } from '../utils'
|
||||
|
||||
export let _class: Ref<Class<Employee>> = contact.mixin.Employee
|
||||
export let searchField: string = 'name'
|
||||
export let searchMode: 'field' | 'fulltext' | 'disabled' = 'field'
|
||||
export let groupBy = '_class'
|
||||
export let search: string = ''
|
||||
export let selected: Ref<Employee>[] = []
|
||||
export let skipCurrentAccount = false
|
||||
export let disableDeselectFor: Ref<Employee>[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
const focusManager = createFocusManager()
|
||||
const currentAccount = getCurrentAccount() as PersonAccount
|
||||
|
||||
let listSelection = 0
|
||||
let list: ListView
|
||||
|
||||
let selectedItems = new Set<Ref<Employee>>(selected)
|
||||
|
||||
let persons: Employee[] = []
|
||||
|
||||
$: currentPerson = $personByIdStore.get(currentAccount.person)
|
||||
|
||||
$: query.query(
|
||||
_class,
|
||||
{
|
||||
...(searchMode !== 'disabled' && search !== ''
|
||||
? searchMode === 'fulltext'
|
||||
? { $search: search }
|
||||
: { [searchField]: { $like: '%' + search + '%' } }
|
||||
: {}),
|
||||
...(skipCurrentAccount && currentPerson ? { _id: { $ne: currentPerson._id as Ref<Employee> } } : {})
|
||||
},
|
||||
(result) => {
|
||||
result.sort((a, b) => {
|
||||
const aval: string = `${getObjectValue(groupBy, a as any)}`
|
||||
const bval: string = `${getObjectValue(groupBy, b as any)}`
|
||||
return aval.localeCompare(bval)
|
||||
})
|
||||
persons = result
|
||||
},
|
||||
{ limit: 200 }
|
||||
)
|
||||
|
||||
function handleSelection (contacts: Employee[], index: number) {
|
||||
const contact = contacts[index]
|
||||
|
||||
if (contact === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedItems = flipSet(selectedItems, contact._id)
|
||||
|
||||
dispatch('select', Array.from(selectedItems))
|
||||
}
|
||||
|
||||
function onKeydown (key: KeyboardEvent): void {
|
||||
if (key.code === 'ArrowUp') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(listSelection - 1)
|
||||
}
|
||||
if (key.code === 'ArrowDown') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(listSelection + 1)
|
||||
}
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
handleSelection(persons, listSelection)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusHandler manager={focusManager} />
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="w-full" tabindex="0" on:keydown={onKeydown}>
|
||||
<ListView bind:this={list} count={persons.length} bind:selection={listSelection} colorsSchema="lumia" noScroll>
|
||||
<svelte:fragment slot="item" let:item={index}>
|
||||
{@const person = persons[index]}
|
||||
{@const disabled = selectedItems.has(person._id) && disableDeselectFor.includes(person._id)}
|
||||
<button
|
||||
class="row withList w-full"
|
||||
class:cursor-default={disabled}
|
||||
{disabled}
|
||||
on:click={() => {
|
||||
handleSelection(persons, index)
|
||||
}}
|
||||
>
|
||||
<UserDetails avatarSize="small" {person} />
|
||||
<CheckBox
|
||||
checked={selectedItems.has(person._id)}
|
||||
readonly={disabled}
|
||||
kind="primary"
|
||||
on:value={() => {
|
||||
handleSelection(persons, index)
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-1) var(--spacing-2) var(--spacing-1) var(--spacing-1);
|
||||
flex-grow: 1;
|
||||
border-radius: var(--small-BorderRadius);
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,32 @@
|
||||
<!--
|
||||
// Copyright © 2024 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M5 8C7.20914 8 9 6.20914 9 4C9 1.79086 7.20914 0 5 0C2.79086 0 1 1.79086 1 4C1 6.20914 2.79086 8 5 8ZM5 7C6.65685 7 8 5.65685 8 4C8 2.34315 6.65685 1 5 1C3.34315 1 2 2.34315 2 4C2 5.65685 3.34315 7 5 7Z"
|
||||
/>
|
||||
<path
|
||||
d="M9 9.5C9 9.22386 9.22386 9 9.5 9C9.77614 9 10 9.22386 10 9.5V11H11.5C11.7761 11 12 11.2239 12 11.5C12 11.7761 11.7761 12 11.5 12H10V13.5C10 13.7761 9.77614 14 9.5 14C9.22386 14 9 13.7761 9 13.5V12H7.5C7.22386 12 7 11.7761 7 11.5C7 11.2239 7.22386 11 7.5 11H9V9.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9C1.567 9 0 10.567 0 12.5V13.5C0 13.7761 0.223858 14 0.5 14C0.776142 14 1 13.7761 1 13.5V12.5C1 11.1193 2.11929 10 3.5 10H5.5C5.77614 10 6 9.77614 6 9.5C6 9.22386 5.77614 9 5.5 9H3.5Z"
|
||||
/>
|
||||
</svg>
|
@ -99,6 +99,10 @@ import TxNameChange from './components/activity/TxNameChange.svelte'
|
||||
import NameChangedActivityMessage from './components/activity/NameChangedActivityMessage.svelte'
|
||||
import SystemAvatar from './components/SystemAvatar.svelte'
|
||||
import PersonIcon from './components/PersonIcon.svelte'
|
||||
import UsersList from './components/UsersList.svelte'
|
||||
import SelectUsersPopup from './components/SelectUsersPopup.svelte'
|
||||
import IconAddMember from './components/icons/AddMember.svelte'
|
||||
import UserDetails from './components/UserDetails.svelte'
|
||||
|
||||
import contact from './plugin'
|
||||
import {
|
||||
@ -160,7 +164,11 @@ export {
|
||||
MembersBox,
|
||||
PersonRefPresenter,
|
||||
SystemAvatar,
|
||||
PersonIcon
|
||||
PersonIcon,
|
||||
UsersList,
|
||||
SelectUsersPopup,
|
||||
IconAddMember,
|
||||
UserDetails
|
||||
}
|
||||
|
||||
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
|
||||
|
@ -269,7 +269,8 @@ export const contactPlugin = plugin(contactId, {
|
||||
PersonLastNamePlaceholder: '' as IntlString,
|
||||
NumberMembers: '' as IntlString,
|
||||
Position: '' as IntlString,
|
||||
For: '' as IntlString
|
||||
For: '' as IntlString,
|
||||
SelectUsers: '' as IntlString
|
||||
},
|
||||
viewlet: {
|
||||
TableMember: '' as Ref<Viewlet>,
|
||||
|
@ -100,4 +100,7 @@
|
||||
<path d="M15 4.5C15 4.22386 14.7761 4 14.5 4H12.95C12.7 2.85 11.7 2 10.5 2C9.3 2 8.3 2.85 8.05 4H1.5C1.22386 4 1 4.22386 1 4.5C1 4.77614 1.22386 5 1.5 5H8.05C8.3 6.15 9.3 7 10.5 7C11.7 7 12.7 6.15 12.95 5H14.5C14.7761 5 15 4.77614 15 4.5ZM10.5 6C9.65 6 9 5.35 9 4.5C9 3.65 9.65 3 10.5 3C11.35 3 12 3.65 12 4.5C12 5.35 11.35 6 10.5 6Z"/>
|
||||
<path d="M1 11.5C1 11.7761 1.22386 12 1.5 12H3.05C3.3 13.15 4.3 14 5.5 14C6.7 14 7.7 13.15 7.95 12H14.5C14.7761 12 15 11.7761 15 11.5C15 11.2239 14.7761 11 14.5 11H7.95C7.7 9.85 6.7 9 5.5 9C4.3 9 3.3 9.85 3.05 11H1.5C1.22386 11 1 11.2239 1 11.5ZM5.5 10C6.35 10 7 10.65 7 11.5C7 12.35 6.35 13 5.5 13C4.65 13 4 12.35 4 11.5C4 10.65 4.65 10 5.5 10Z"/>
|
||||
</symbol>
|
||||
<symbol id="database" viewBox="0 0 12 14">
|
||||
<path d="M6 0.5C3.35105 0.5 0.5 1.126 0.5 2.5V11.5C0.5 12.874 3.35105 13.5 6 13.5C8.64895 13.5 11.5 12.874 11.5 11.5V2.5C11.5 1.126 8.64895 0.5 6 0.5ZM6 1.5C8.8988 1.5 10.3974 2.21705 10.4984 2.5C10.3974 2.78295 8.8988 3.5 6 3.5C3.07935 3.5 1.5803 2.7722 1.5 2.5088V2.50635C1.5803 2.2278 3.07935 1.5 6 1.5ZM1.5 3.71385C2.56395 4.24755 4.3213 4.5 6 4.5C7.6787 4.5 9.43605 4.24755 10.5 3.71385V5.49365C10.4197 5.7722 8.92065 6.5 6 6.5C3.07495 6.5 1.57545 5.77 1.5 5.5V3.71385ZM1.5 6.71385C2.56395 7.24755 4.3213 7.5 6 7.5C7.6787 7.5 9.43605 7.24755 10.5 6.71385V8.49365C10.4197 8.7722 8.92065 9.5 6 9.5C3.07495 9.5 1.57545 8.77 1.5 8.5V6.71385ZM6 12.5C3.07495 12.5 1.57545 11.77 1.5 11.5V9.71385C2.56395 10.2476 4.3213 10.5 6 10.5C7.6787 10.5 9.43605 10.2476 10.5 9.71385V11.4937C10.4197 11.7722 8.92065 12.5 6 12.5Z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
@ -39,5 +39,6 @@ loadMetadata(view.icon, {
|
||||
DevModel: `${icons}#devmodel`,
|
||||
ViewButton: `${icons}#viewButton`,
|
||||
Filter: `${icons}#filter`,
|
||||
Configure: `${icons}#configure`
|
||||
Configure: `${icons}#configure`,
|
||||
Database: `${icons}#database`
|
||||
})
|
||||
|
@ -925,7 +925,8 @@ const view = plugin(viewId, {
|
||||
DevModel: '' as Asset,
|
||||
ViewButton: '' as Asset,
|
||||
Filter: '' as Asset,
|
||||
Configure: '' as Asset
|
||||
Configure: '' as Asset,
|
||||
Database: '' as Asset
|
||||
},
|
||||
category: {
|
||||
General: '' as Ref<ActionCategory>,
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
import chunter, {
|
||||
Backlink,
|
||||
Channel,
|
||||
ChatMessage,
|
||||
chunterId,
|
||||
ChunterSpace,
|
||||
@ -230,36 +231,47 @@ async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): P
|
||||
return []
|
||||
}
|
||||
|
||||
const res: Tx[] = []
|
||||
const targetDoc = (
|
||||
await control.findAll(chatMessage.attachedToClass, { _id: chatMessage.attachedTo }, { limit: 1 })
|
||||
)[0]
|
||||
|
||||
if (targetDoc !== undefined) {
|
||||
if (hierarchy.hasMixin(targetDoc, notification.mixin.Collaborators)) {
|
||||
const collaboratorsMixin = hierarchy.as(targetDoc, notification.mixin.Collaborators)
|
||||
if (!collaboratorsMixin.collaborators.includes(chatMessage.modifiedBy)) {
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(
|
||||
targetDoc._id,
|
||||
targetDoc._class,
|
||||
targetDoc.space,
|
||||
notification.mixin.Collaborators,
|
||||
{
|
||||
$push: {
|
||||
collaborators: chatMessage.modifiedBy
|
||||
}
|
||||
if (targetDoc === undefined) {
|
||||
return []
|
||||
}
|
||||
const res: Tx[] = []
|
||||
const isChannel = hierarchy.isDerived(targetDoc._class, chunter.class.Channel)
|
||||
|
||||
if (hierarchy.hasMixin(targetDoc, notification.mixin.Collaborators)) {
|
||||
const collaboratorsMixin = hierarchy.as(targetDoc, notification.mixin.Collaborators)
|
||||
if (!collaboratorsMixin.collaborators.includes(chatMessage.modifiedBy)) {
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(
|
||||
targetDoc._id,
|
||||
targetDoc._class,
|
||||
targetDoc.space,
|
||||
notification.mixin.Collaborators,
|
||||
{
|
||||
$push: {
|
||||
collaborators: chatMessage.modifiedBy
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const collaborators = await getDocCollaborators(targetDoc, mixin, control)
|
||||
if (!collaborators.includes(chatMessage.modifiedBy)) {
|
||||
collaborators.push(chatMessage.modifiedBy)
|
||||
}
|
||||
res.push(getMixinTx(tx, control, collaborators))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const collaborators = await getDocCollaborators(targetDoc, mixin, control)
|
||||
if (!collaborators.includes(chatMessage.modifiedBy)) {
|
||||
collaborators.push(chatMessage.modifiedBy)
|
||||
}
|
||||
res.push(getMixinTx(tx, control, collaborators))
|
||||
}
|
||||
|
||||
if (isChannel && !(targetDoc as Channel).members.includes(chatMessage.modifiedBy)) {
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(targetDoc._class, targetDoc.space, targetDoc._id, {
|
||||
$push: { members: chatMessage.modifiedBy }
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return res
|
||||
@ -532,13 +544,89 @@ async function OnChatMessageRemoved (tx: TxCollectionCUD<Doc, ChatMessage>, cont
|
||||
return res
|
||||
}
|
||||
|
||||
function combineAttributes (attributes: any[], key: string, operator: string, arrayKey: string): any[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
attributes.flatMap((attr) =>
|
||||
Array.isArray(attr[operator]?.[key]?.[arrayKey]) ? attr[operator]?.[key]?.[arrayKey] : attr[operator]?.[key]
|
||||
)
|
||||
)
|
||||
).filter((v) => v != null)
|
||||
}
|
||||
|
||||
async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: TriggerControl): Promise<Tx[]> {
|
||||
const changedAttributes = Object.entries(tx.operations)
|
||||
.flatMap(([id, val]) => (['$push', '$pull'].includes(id) ? Object.keys(val) : id))
|
||||
.filter((id) => !id.startsWith('$'))
|
||||
|
||||
if (!changedAttributes.includes('members')) {
|
||||
return []
|
||||
}
|
||||
|
||||
const added = combineAttributes([tx.operations], 'members', '$push', '$each')
|
||||
const removed = combineAttributes([tx.operations], 'members', '$pull', '$in')
|
||||
|
||||
const res: Tx[] = []
|
||||
const allContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: tx.objectId })
|
||||
|
||||
if (removed.length > 0) {
|
||||
res.push(
|
||||
control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, {
|
||||
$pull: {
|
||||
collaborators: { $in: removed }
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
for (const addedMember of added) {
|
||||
const context = allContexts.find(({ user }) => user === addedMember)
|
||||
|
||||
if (context === undefined) {
|
||||
res.push(
|
||||
control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, tx.objectSpace, {
|
||||
attachedTo: tx.objectId,
|
||||
attachedToClass: tx.objectClass,
|
||||
user: addedMember,
|
||||
hidden: false,
|
||||
lastViewedTimestamp: tx.modifiedOn
|
||||
})
|
||||
)
|
||||
} else {
|
||||
res.push(
|
||||
control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
||||
hidden: false,
|
||||
lastViewedTimestamp: tx.modifiedOn
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const contextsToRemove = allContexts.filter(({ user }) => removed.includes(user))
|
||||
|
||||
if (contextsToRemove.length === 0) {
|
||||
return res
|
||||
}
|
||||
|
||||
const channel = (await control.findAll(chunter.class.Channel, { _id: tx.objectId }))[0]
|
||||
|
||||
if (channel !== undefined) {
|
||||
for (const context of contextsToRemove) {
|
||||
res.push(control.txFactory.createTxRemoveDoc(context._class, context.space, context._id))
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
BacklinkTrigger,
|
||||
ChunterTrigger,
|
||||
OnDirectMessageSent,
|
||||
OnChatMessageRemoved
|
||||
OnChatMessageRemoved,
|
||||
OnChannelMembersChanged
|
||||
},
|
||||
function: {
|
||||
CommentRemove,
|
||||
|
@ -31,7 +31,8 @@ export default plugin(serverChunterId, {
|
||||
BacklinkTrigger: '' as Resource<TriggerFunc>,
|
||||
ChunterTrigger: '' as Resource<TriggerFunc>,
|
||||
OnDirectMessageSent: '' as Resource<TriggerFunc>,
|
||||
OnChatMessageRemoved: '' as Resource<TriggerFunc>
|
||||
OnChatMessageRemoved: '' as Resource<TriggerFunc>,
|
||||
OnChannelMembersChanged: '' as Resource<TriggerFunc>
|
||||
},
|
||||
function: {
|
||||
CommentRemove: '' as Resource<ObjectDDParticipantFunc>,
|
||||
|
Loading…
Reference in New Issue
Block a user