Update chat navigator (#4941)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-03-13 07:01:11 +04:00 committed by GitHub
parent cadde5a4b5
commit fb6410fc74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 924 additions and 633 deletions

View File

@ -58,8 +58,8 @@ import {
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
import notification from '@hcengineering/model-notification'
import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import notification, { notificationActionTemplates } from '@hcengineering/model-notification'
import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import type { IntlString, Resource } from '@hcengineering/platform'
@ -82,7 +82,7 @@ export class TChunterSpace extends TSpace implements ChunterSpace {
}
@Model(chunter.class.Channel, chunter.class.ChunterSpace)
@UX(chunter.string.Channel, chunter.icon.Hashtag)
@UX(chunter.string.Channel, chunter.icon.Hashtag, undefined, undefined, undefined, chunter.string.Channels)
export class TChannel extends TChunterSpace implements Channel {
@Prop(TypeString(), chunter.string.Topic)
@Index(IndexKind.FullText)
@ -90,7 +90,7 @@ export class TChannel extends TChunterSpace implements Channel {
}
@Model(chunter.class.DirectMessage, chunter.class.ChunterSpace)
@UX(chunter.string.DirectMessage, contact.icon.Person)
@UX(chunter.string.DirectMessage, contact.icon.Person, undefined, undefined, undefined, chunter.string.DirectMessages)
export class TDirectMessage extends TChunterSpace implements DirectMessage {}
@Model(chunter.class.ChunterMessage, core.class.AttachedDoc, DOMAIN_CHUNTER)
@ -193,6 +193,19 @@ export class TObjectChatPanel extends TClass implements ObjectChatPanel {
titleProvider!: Resource<(object: Doc) => string>
}
const actionTemplates = template({
removeChannel: {
action: chunter.actionImpl.RemoveChannel,
label: view.string.Archive,
icon: view.icon.Delete,
input: 'focus',
keyBinding: ['Backspace'],
category: chunter.category.Chunter,
target: notification.class.DocNotifyContext,
context: { mode: ['context', 'browser'], group: 'remove' }
}
})
export function createModel (builder: Builder, options = { addApplication: true }): void {
builder.createModel(
TChunterSpace,
@ -550,6 +563,42 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.action.DeleteChatMessage
)
createAction(
builder,
{
...actionTemplates.removeChannel,
query: {
attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] }
}
},
chunter.action.RemoveChannel
)
createAction(
builder,
{
...actionTemplates.removeChannel,
label: chunter.string.CloseConversation,
query: {
attachedToClass: chunter.class.DirectMessage
}
},
chunter.action.CloseConversation
)
createAction(
builder,
{
...actionTemplates.removeChannel,
action: chunter.actionImpl.LeaveChannel,
label: chunter.string.LeaveChannel,
query: {
attachedToClass: chunter.class.Channel
}
},
chunter.action.LeaveChannel
)
createAction(
builder,
{
@ -564,6 +613,53 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.action.OpenChannel
)
createAction(builder, {
...notificationActionTemplates.pinContext,
label: chunter.string.StarChannel,
query: {
attachedToClass: chunter.class.Channel
},
override: [notification.action.PinDocNotifyContext]
})
createAction(builder, {
...notificationActionTemplates.unpinContext,
label: chunter.string.UnstarChannel,
query: {
attachedToClass: chunter.class.Channel
}
})
createAction(builder, {
...notificationActionTemplates.pinContext,
label: chunter.string.StarConversation,
query: {
attachedToClass: chunter.class.DirectMessage
}
})
createAction(builder, {
...notificationActionTemplates.unpinContext,
label: chunter.string.UnstarConversation,
query: {
attachedToClass: chunter.class.DirectMessage
}
})
createAction(builder, {
...notificationActionTemplates.pinContext,
query: {
attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] }
}
})
createAction(builder, {
...notificationActionTemplates.unpinContext,
query: {
attachedToClass: { $nin: [chunter.class.DirectMessage, chunter.class.Channel] }
}
})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: chunter.class.Channel,
components: { input: chunter.component.ChatMessageInput }

View File

@ -99,7 +99,7 @@ export class TChannelProvider extends TDoc implements ChannelProvider {
}
@Model(contact.class.Contact, core.class.Doc, DOMAIN_CONTACT)
@UX(contact.string.Contact, contact.icon.Person, 'CONT', 'name')
@UX(contact.string.Contact, contact.icon.Person, 'CONT', 'name', undefined, contact.string.Persons)
export class TContact extends TDoc implements Contact {
@Prop(TypeString(), contact.string.Name)
@Index(IndexKind.FullText)
@ -142,7 +142,7 @@ export class TChannel extends TAttachedDoc implements Channel {
}
@Model(contact.class.Person, contact.class.Contact)
@UX(contact.string.Person, contact.icon.Person, 'PRSN', 'name')
@UX(contact.string.Person, contact.icon.Person, 'PRSN', 'name', undefined, contact.string.Persons)
export class TPerson extends TContact implements Person {
@Prop(TypeDate(DateRangeMode.DATE, false), contact.string.Birthday)
birthday?: Timestamp
@ -156,7 +156,7 @@ export class TMember extends TAttachedDoc implements Member {
}
@Model(contact.class.Organization, contact.class.Contact)
@UX(contact.string.Organization, contact.icon.Company, 'ORG', 'name')
@UX(contact.string.Organization, contact.icon.Company, 'ORG', 'name', undefined, contact.string.Organizations)
export class TOrganization extends TContact implements Organization {
@Prop(TypeCollaborativeMarkup(), core.string.Description)
@Index(IndexKind.FullText)

View File

@ -69,7 +69,7 @@ export class TDocumentEmbedding extends TAttachment implements DocumentEmbedding
}
@Model(document.class.Document, core.class.AttachedDoc, DOMAIN_DOCUMENT)
@UX(document.string.Document, document.icon.Document, undefined, 'name')
@UX(document.string.Document, document.icon.Document, undefined, 'name', undefined, document.string.Documents)
export class TDocument extends TAttachedDoc implements Document {
@Prop(TypeRef(document.class.Document), document.string.ParentDocument)
declare attachedTo: Ref<Document>

View File

@ -32,7 +32,7 @@ export { default } from './plugin'
export const DOMAIN_INVENTORY = 'inventory' as Domain
@Model(inventory.class.Category, core.class.AttachedDoc, DOMAIN_INVENTORY)
@UX(inventory.string.Category, inventory.icon.Categories, undefined, 'name')
@UX(inventory.string.Category, inventory.icon.Categories, undefined, 'name', undefined, inventory.string.Categories)
export class TCategory extends TAttachedDoc implements Category {
@Prop(TypeString(), core.string.Name)
@Index(IndexKind.FullText)
@ -40,7 +40,7 @@ export class TCategory extends TAttachedDoc implements Category {
}
@Model(inventory.class.Product, core.class.AttachedDoc, DOMAIN_INVENTORY)
@UX(inventory.string.Product, inventory.icon.Products, undefined, 'name')
@UX(inventory.string.Product, inventory.icon.Products, undefined, 'name', undefined, inventory.string.Products)
export class TProduct extends TAttachedDoc implements Product {
// We need to declare, to provide property with label
@Prop(TypeRef(inventory.class.Category), inventory.string.Category)
@ -61,7 +61,7 @@ export class TProduct extends TAttachedDoc implements Product {
}
@Model(inventory.class.Variant, core.class.AttachedDoc, DOMAIN_INVENTORY)
@UX(inventory.string.Variant, inventory.icon.Variant, undefined, 'name')
@UX(inventory.string.Variant, inventory.icon.Variant, undefined, 'name', undefined, inventory.string.Variants)
export class TVariant extends TAttachedDoc implements Variant {
// We need to declare, to provide property with label
@Prop(TypeRef(inventory.class.Product), inventory.string.Product)

View File

@ -67,7 +67,7 @@ export class TFunnel extends TProject implements Funnel {
}
@Model(lead.class.Lead, task.class.Task)
@UX(lead.string.Lead, lead.icon.Lead, 'LEAD', 'title')
@UX(lead.string.Lead, lead.icon.Lead, 'LEAD', 'title', undefined, lead.string.Leads)
export class TLead extends TTask implements Lead {
@Prop(TypeRef(contact.class.Contact), lead.string.Customer)
@ReadOnly()
@ -90,7 +90,7 @@ export class TLead extends TTask implements Lead {
}
@Mixin(lead.mixin.Customer, contact.class.Contact)
@UX(lead.string.Customer, lead.icon.LeadApplication)
@UX(lead.string.Customer, lead.icon.LeadApplication, undefined, undefined, undefined, lead.string.Customers)
export class TCustomer extends TContact implements Customer {
@Prop(Collection(lead.class.Lead), lead.string.Leads)
leads?: number

View File

@ -49,7 +49,7 @@ import {
} from '@hcengineering/model'
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view'
import view, { createAction, template } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import {
type DocUpdates,
@ -274,6 +274,29 @@ export class TActivityNotificationViewlet extends TDoc implements ActivityNotifi
presenter!: AnyComponent
}
export const notificationActionTemplates = template({
pinContext: {
action: notification.actionImpl.PinDocNotifyContext,
label: notification.string.StarDocument,
icon: view.icon.Star,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
visibilityTester: notification.function.HasDocNotifyContextPinAction,
context: { mode: ['context', 'browser'], group: 'edit' }
},
unpinContext: {
action: notification.actionImpl.UnpinDocNotifyContext,
label: notification.string.UnstarDocument,
icon: view.icon.Star,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
visibilityTester: notification.function.HasDocNotifyContextUnpinAction,
context: { mode: ['context', 'browser'], group: 'edit' }
}
})
export function createModel (builder: Builder): void {
builder.createModel(
TNotification,
@ -570,36 +593,6 @@ export function createModel (builder: Builder): void {
notification.action.UnHideDocNotifyContext
)
createAction(
builder,
{
action: notification.actionImpl.PinDocNotifyContext,
label: view.string.Pin,
icon: notification.icon.Track,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
visibilityTester: notification.function.HasDocNotifyContextPinAction,
context: { mode: ['context', 'browser'], group: 'edit' }
},
notification.action.PinDocNotifyContext
)
createAction(
builder,
{
action: notification.actionImpl.UnpinDocNotifyContext,
label: view.string.Unpin,
icon: notification.icon.Track,
input: 'focus',
category: notification.category.Notification,
target: notification.class.DocNotifyContext,
visibilityTester: notification.function.HasDocNotifyContextUnpinAction,
context: { mode: ['context', 'browser'], group: 'edit' }
},
notification.action.UnpinDocNotifyContext
)
builder.mixin(notification.class.DocNotifyContext, core.class.Class, view.mixin.ObjectPresenter, {
presenter: notification.component.DocNotifyContextPresenter
})

View File

@ -32,7 +32,9 @@ export default mergeIds(notificationId, notification, {
MarkAsUnread: '' as IntlString,
MarkAsRead: '' as IntlString,
ChangeCollaborators: '' as IntlString,
Message: '' as IntlString
Message: '' as IntlString,
StarDocument: '' as IntlString,
UnstarDocument: '' as IntlString
},
app: {
Notification: '' as Ref<Application>,

View File

@ -68,7 +68,7 @@ export { recruitOperation } from './migration'
export { default } from './plugin'
@Model(recruit.class.Vacancy, task.class.Project)
@UX(recruit.string.Vacancy, recruit.icon.Vacancy, 'VCN', 'name')
@UX(recruit.string.Vacancy, recruit.icon.Vacancy, 'VCN', 'name', undefined, recruit.string.Vacancies)
export class TVacancy extends TProject implements Vacancy {
@Prop(TypeCollaborativeMarkup(), recruit.string.FullDescription)
@Index(IndexKind.FullText)
@ -101,7 +101,7 @@ export class TVacancy extends TProject implements Vacancy {
export class TCandidates extends TSpace implements Candidates {}
@Mixin(recruit.mixin.Candidate, contact.class.Person)
@UX(recruit.string.Talent, recruit.icon.RecruitApplication, 'TLNT', 'name')
@UX(recruit.string.Talent, recruit.icon.RecruitApplication, 'TLNT', 'name', undefined, recruit.string.Talents)
export class TCandidate extends TPerson implements Candidate {
@Prop(TypeString(), recruit.string.Title)
@Index(IndexKind.FullText)
@ -146,7 +146,7 @@ export class TVacancyList extends TOrganization implements VacancyList {
}
@Model(recruit.class.Applicant, task.class.Task)
@UX(recruit.string.Application, recruit.icon.Application, 'APP', 'number')
@UX(recruit.string.Application, recruit.icon.Application, 'APP', 'number', undefined, recruit.string.Applications)
export class TApplicant extends TTask implements Applicant {
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.mixin.Candidate), recruit.string.Talent)

View File

@ -12,7 +12,7 @@ import chunter from '@hcengineering/model-chunter'
import recruit from './plugin'
@Model(recruit.class.Review, calendar.class.Event)
@UX(recruit.string.Review, recruit.icon.Review, 'RVE', 'number')
@UX(recruit.string.Review, recruit.icon.Review, 'RVE', 'number', undefined, recruit.string.Reviews)
export class TReview extends TEvent implements Review {
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.mixin.Candidate), recruit.string.Talent)

View File

@ -163,7 +163,7 @@ export function TypeEstimation (): Type<number> {
* @public
*/
@Model(tracker.class.Issue, task.class.Task)
@UX(tracker.string.Issue, tracker.icon.Issue, 'TSK', 'title')
@UX(tracker.string.Issue, tracker.icon.Issue, 'TSK', 'title', undefined, tracker.string.Issues)
export class TIssue extends TTask implements Issue {
@Prop(TypeRef(tracker.class.Issue), tracker.string.Parent)
declare attachedTo: Ref<Issue>
@ -249,7 +249,14 @@ export class TIssue extends TTask implements Issue {
*/
@Model(tracker.class.IssueTemplate, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.IssueTemplate, tracker.icon.IssueTemplates, 'PROCESS')
@UX(
tracker.string.IssueTemplate,
tracker.icon.IssueTemplates,
'PROCESS',
undefined,
undefined,
tracker.string.IssueTemplates
)
export class TIssueTemplate extends TDoc implements IssueTemplate {
@Prop(TypeString(), tracker.string.Title)
@Index(IndexKind.FullText)
@ -324,7 +331,7 @@ export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport {
*/
@Model(tracker.class.Component, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Component, tracker.icon.Component, 'COMPONENT', 'label')
@UX(tracker.string.Component, tracker.icon.Component, 'COMPONENT', 'label', undefined, tracker.string.Components)
export class TComponent extends TDoc implements Component {
@Prop(TypeString(), tracker.string.Title)
@Index(IndexKind.FullText)
@ -349,7 +356,7 @@ export class TComponent extends TDoc implements Component {
* @public
*/
@Model(tracker.class.Milestone, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.Milestone, tracker.icon.Milestone, '', 'label')
@UX(tracker.string.Milestone, tracker.icon.Milestone, '', 'label', undefined, tracker.string.Milestones)
export class TMilestone extends TDoc implements Milestone {
@Prop(TypeString(), tracker.string.Title)
// @Index(IndexKind.FullText)

View File

@ -188,6 +188,7 @@ export interface Class<T extends Obj> extends Classifier {
shortLabel?: string
sortingKey?: string
filteringKey?: string
pluralLabel?: IntlString
}
/**

View File

@ -80,6 +80,7 @@ interface ClassTxes {
shortLabel?: string | IntlString
sortingKey?: string
filteringKey?: string
pluralLabel?: IntlString
}
const transactions = new Map<any, ClassTxes>()
@ -227,7 +228,8 @@ export function UX<T extends Obj> (
icon?: Asset,
shortLabel?: string,
sortingKey?: string,
filteringKey?: string
filteringKey?: string,
pluralLabel?: IntlString
) {
return function classDecorator<C extends new () => T> (constructor: C): void {
const txes = getTxes(constructor.prototype)
@ -236,6 +238,7 @@ export function UX<T extends Obj> (
txes.shortLabel = shortLabel
txes.sortingKey = sortingKey
txes.filteringKey = filteringKey ?? sortingKey
txes.pluralLabel = pluralLabel
}
}
@ -273,7 +276,8 @@ function _generateTx (tx: ClassTxes): Tx[] {
icon: tx.icon,
shortLabel: tx.shortLabel,
sortingKey: tx.sortingKey,
filteringKey: tx.filteringKey
filteringKey: tx.filteringKey,
pluralLabel: tx.pluralLabel
},
objectId
)

View File

@ -981,6 +981,7 @@ a.no-line {
.background-content-accent-color { background-color: var(--accent-color); }
.background-comp-header-color { background-color: var(--theme-comp-header-color) !important; }
.background-navpanel-color { background-color: var(--theme-navpanel-color) !important; }
.background-surface-color { background-color: var(--global-surface-01-BackgroundColor) !important; }
.content-trans-color { color: var(--theme-trans-color); }
.content-darker-color { color: var(--theme-darker-color); }

View File

@ -6,14 +6,12 @@
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])
}

View File

@ -5,7 +5,7 @@
//
import type { Asset, IntlString } from '@hcengineering/platform'
import { AnySvelteComponent, IconSize } from '../types'
import { AnySvelteComponent, ButtonBaseSize, IconSize } from '../types'
import { ComponentType } from 'svelte'
import ButtonBase from './ButtonBase.svelte'
@ -13,7 +13,7 @@
export let label: IntlString | undefined = undefined
export let labelParams: Record<string, any> = {}
export let kind: 'primary' | 'secondary' | 'tertiary' | 'negative' = 'secondary'
export let size: 'large' | 'medium' | 'small' = 'large'
export let size: ButtonBaseSize = 'large'
export let icon: Asset | AnySvelteComponent | ComponentType | undefined = undefined
export let iconSize: IconSize | undefined = undefined
export let disabled: boolean = false

View File

@ -108,10 +108,6 @@
{:else if item.labelIntl}
<Label label={item.labelIntl} params={item.labelParams} />
{/if}
{#if item.showNotify}
<div class="notifyMarker" />
{/if}
</span>
{/if}
</div>
@ -289,12 +285,4 @@
}
}
}
.notifyMarker {
width: 4px;
height: 4px;
border-radius: 1px;
background: var(--global-higlight-Color);
margin-left: 0.5rem;
}
</style>

View File

@ -0,0 +1,24 @@
<!--
// 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 { IconSize } from '../../types'
export let size: IconSize
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 32 32" {fill} xmlns="http://www.w3.org/2000/svg">
<path d="M24 12L16 22L8 12H24Z" />
</svg>

View File

@ -0,0 +1,24 @@
<!--
// 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 { IconSize } from '../../types'
export let size: IconSize
export let fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 32 32" {fill} xmlns="http://www.w3.org/2000/svg">
<path d="M12 8L22 16L12 24V8Z" />
</svg>

View File

@ -211,6 +211,8 @@ export { default as IconSend } from './components/icons/Send.svelte'
export { default as IconSquareExpand } from './components/icons/SquareExpand.svelte'
export { default as IconTableOfContents } from './components/icons/TableOfContents.svelte'
export { default as IconRight } from './components/icons/Right.svelte'
export { default as IconDropdownDown } from './components/icons/DropdownDown.svelte'
export { default as IconDropdownRight } from './components/icons/DropdownRight.svelte'
export { default as PanelInstance } from './components/PanelInstance.svelte'
export { default as Panel } from './components/Panel.svelte'

View File

@ -122,7 +122,6 @@ export interface TabItem {
icon?: Asset | AnySvelteComponent
color?: string
tooltip?: IntlString
showNotify?: boolean
action?: () => void
}

View File

@ -98,6 +98,13 @@
"LoadingHistory": "Loading history...",
"UnpinChannels": "Unpin all channels",
"ArchiveActivityConfirmationTitle": "Archive all activity channels?",
"ArchiveActivityConfirmationMessage": "Are you sure you want to archive all activity channels? This operation cannot be undone."
"ArchiveActivityConfirmationMessage": "Are you sure you want to archive all activity channels? This operation cannot be undone.",
"CloseConversation": "Close conversation",
"Starred": "Starred",
"DeleteStarred": "Delete starred",
"StarChannel": "Star channel",
"StarConversation": "Star conversation",
"UnstarChannel": "Unstar channel",
"UnstarConversation": "Unstar conversation"
}
}

View File

@ -98,6 +98,13 @@
"LoadingHistory": "Загрузка истории...",
"UnpinChannels": "Открепить все каналы",
"ArchiveActivityConfirmationTitle": "Архивировать все каналы активности?",
"ArchiveActivityConfirmationMessage": "Вы уверены, что хотите заархивировать все каналы активности? Эту операцию невозможно отменить."
"ArchiveActivityConfirmationMessage": "Вы уверены, что хотите заархивировать все каналы активности? Эту операцию невозможно отменить.",
"CloseConversation": "Закрыть диалог",
"Starred": "Избранное",
"DeleteStarred": "Удалить избранное",
"StarChannel": "Добавить в избранное",
"StarConversation": "Добавить в избранное",
"UnstarChannel": "Удалить из избранного",
"UnstarConversation": "Удалить из избранного"
}
}

View File

@ -124,17 +124,10 @@
<div class="flex-row-top h-full">
{#if visibleNav}
<div
class="antiPanel-navigator {appsDirection === 'horizontal'
? 'portrait'
: 'landscape'} background-comp-header-color"
class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'} background-surface-color"
>
<div class="antiPanel-wrap__content">
<ChatNavigator
{selectedContextId}
selectedObjectClass={selectedContext?.attachedToClass}
{currentSpecial}
on:select={handleChannelSelected}
/>
<ChatNavigator {selectedContextId} {currentSpecial} on:select={handleChannelSelected} />
</div>
<Separator name="chat" float={navFloat ? 'navigator' : true} index={0} />
</div>

View File

@ -16,12 +16,12 @@
import { createEventDispatcher } from 'svelte'
import { ModernEditbox, ButtonMenu, Label, Modal, TextArea } from '@hcengineering/ui'
import presentation, { getClient } from '@hcengineering/presentation'
import { getResource } from '@hcengineering/platform'
import core, { getCurrentAccount } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import Lock from '../../icons/Lock.svelte'
import chunter from '../../../plugin'
import { openChannel } from '../../../index'
const dispatch = createEventDispatcher()
const client = getClient()
@ -50,7 +50,7 @@
$: canSave = !!channelName
async function save () {
async function save (): Promise<void> {
const accountId = getCurrentAccount()._id
const channelId = await client.createDoc(chunter.class.Channel, core.space.Space, {
name: channelName,
@ -67,12 +67,10 @@
hidden: false
})
const openChannelFn = await getResource(chunter.actionImpl.OpenChannel)
await openChannelFn(undefined, undefined, { _id: notifyContextId, mode: 'channels' })
await openChannel(undefined, undefined, { _id: notifyContextId })
}
function handleCancel () {
function handleCancel (): void {
dispatch('close')
}
</script>

View File

@ -19,15 +19,15 @@
import { DirectMessage } from '@hcengineering/chunter'
import contact, { Employee, PersonAccount } from '@hcengineering/contact'
import core, { getCurrentAccount, Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { SelectUsersPopup } from '@hcengineering/contact-resources'
import notification, { DocNotifyContext } from '@hcengineering/notification'
import notification 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'
import { openChannel } from '../../../index'
const dispatch = createEventDispatcher()
const client = getClient()
@ -39,50 +39,55 @@
let accounts: PersonAccount[] = []
let hidden = true
$: loadDmName(accounts).then((r) => {
$: void loadDmName(accounts).then((r) => {
dmName = r
})
$: query.query(contact.class.PersonAccount, { person: { $in: employeeIds } }, (res) => {
accounts = res
})
async function loadDmName (employeeAccounts: PersonAccount[]) {
async function loadDmName (employeeAccounts: PersonAccount[]): Promise<string> {
return await buildDmName(client, employeeAccounts)
}
async function createDirectMessage () {
async function createDirectMessage (): Promise<void> {
const employeeAccounts = await client.findAll(contact.class.PersonAccount, { person: { $in: employeeIds } })
const accIds = [myAccId, ...employeeAccounts.filter(({ _id }) => _id !== myAccId).map(({ _id }) => _id)].sort()
const existingContexts = await client.findAll<DocNotifyContext>(
notification.class.DocNotifyContext,
{
user: myAccId,
attachedToClass: chunter.class.DirectMessage
},
{ lookup: { attachedTo: chunter.class.DirectMessage } }
)
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
const navigate = await getResource(chunter.actionImpl.OpenChannel)
for (const context of existingContexts) {
if (deepEqual((context.$lookup?.attachedTo as DirectMessage)?.members.sort(), accIds)) {
if (context.hidden) {
await client.update(context, { hidden: false })
}
await navigate(context)
return
let direct: DirectMessage | undefined
for (const dm of existingDms) {
if (deepEqual(dm.members.sort(), accIds)) {
direct = dm
break
}
}
const dmId = await client.createDoc(chunter.class.DirectMessage, core.space.Space, {
name: '',
description: '',
private: true,
archived: false,
members: accIds
})
const context = direct
? await client.findOne(notification.class.DocNotifyContext, {
user: myAccId,
attachedTo: direct._id,
attachedToClass: chunter.class.DirectMessage
})
: undefined
if (context !== undefined) {
await client.diffUpdate(context, { hidden: false })
await openChannel(context)
return
}
const dmId =
direct?._id ??
(await client.createDoc(chunter.class.DirectMessage, core.space.Space, {
name: '',
description: '',
private: true,
archived: false,
members: accIds
}))
const notifyContextId = await client.createDoc(notification.class.DocNotifyContext, core.space.Space, {
user: myAccId,
@ -91,10 +96,10 @@
hidden: false
})
await navigate(undefined, undefined, { _id: notifyContextId, mode: 'direct' })
await openChannel(undefined, undefined, { _id: notifyContextId })
}
function handleCancel () {
function handleCancel (): void {
dispatch('close')
}
@ -102,11 +107,11 @@
openSelectUsersPopup(true)
})
function addMembersClicked () {
function addMembersClicked (): void {
openSelectUsersPopup(false)
}
function openSelectUsersPopup (closeOnClose: boolean) {
function openSelectUsersPopup (closeOnClose: boolean): void {
showPopup(
SelectUsersPopup,
{

View File

@ -1,80 +0,0 @@
<!--
// 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 { IntlString } from '@hcengineering/platform'
import { Action, IconMoreH, Label, Menu, showPopup } from '@hcengineering/ui'
export let header: IntlString
export let actions: Action[] = []
async function handleMenuClicked (ev: MouseEvent) {
showPopup(Menu, { actions }, ev.target as HTMLElement)
}
</script>
<div class="root">
<div class="header uppercase">
<Label label={header} />
</div>
<div class="grower" />
{#if actions.length > 0}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="action" on:click|preventDefault|stopPropagation={handleMenuClicked}>
<IconMoreH size="small" />
</div>
{/if}
</div>
<style lang="scss">
.root {
display: flex;
align-items: center;
flex-shrink: 0;
border-radius: 0.375rem;
padding-right: 0.25rem;
}
.header {
padding: 0.5rem;
border-radius: 0.25rem;
background-color: var(--global-ui-highlight-BackgroundColor);
font-size: 0.75rem;
width: fit-content;
margin: 0.5rem 0;
}
.action {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
border-radius: 0.25rem;
flex-shrink: 0;
margin-left: 0.5rem;
cursor: pointer;
&:hover {
color: var(--theme-caption-color);
background-color: var(--global-ui-highlight-BackgroundColor);
}
}
.grower {
flex-grow: 1;
min-width: 0;
}
</style>

View File

@ -13,36 +13,39 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, getCurrentAccount, groupByArray, Ref } from '@hcengineering/core'
import { Class, Doc, getCurrentAccount, groupByArray, Ref, SortingOrder } from '@hcengineering/core'
import notification, { DocNotifyContext } from '@hcengineering/notification'
import { createQuery, getClient, LiveQuery, MessageBox } from '@hcengineering/presentation'
import { Action, Scroller, showPopup } from '@hcengineering/ui'
import { createQuery, getClient, LiveQuery } from '@hcengineering/presentation'
import activity from '@hcengineering/activity'
import view from '@hcengineering/view'
import { getResource } from '@hcengineering/platform'
import { translate } from '@hcengineering/platform'
import { Action } from '@hcengineering/ui'
import ChatNavItem from './ChatNavGroupItem.svelte'
import ChatGroupHeader from './ChatGroupHeader.svelte'
import chunter from '../../../plugin'
import { ChatNavGroupModel } from '../types'
import { readActivityChannels, removeActivityChannels } from '../utils'
import ChatNavSection from './ChatNavSection.svelte'
import chunter from '../../../plugin'
export let selectedContextId: Ref<DocNotifyContext> | undefined = undefined
export let model: ChatNavGroupModel
interface Section {
id: string
_class?: Ref<Class<Doc>>
label: string
objects: Doc[]
}
const client = getClient()
const hierarchy = client.getHierarchy()
const allContextsQuery = createQuery()
const contextsQuery = createQuery()
const objectsQueryByClass = new Map<Ref<Class<Doc>>, LiveQuery>()
let objectsByClass = new Map<Ref<Class<Doc>>, Doc[]>()
let allContexts: DocNotifyContext[] = []
let contexts: DocNotifyContext[] = []
let pinnedContexts: DocNotifyContext[] = []
$: allContextsQuery.query(
let sections: Section[] = []
$: contextsQuery.query(
notification.class.DocNotifyContext,
{
...model.query,
@ -50,25 +53,25 @@
user: getCurrentAccount()._id
},
(res: DocNotifyContext[]) => {
allContexts = sortContexts(
res.filter(
({ attachedToClass }) =>
hierarchy.classHierarchyMixin(attachedToClass, activity.mixin.ActivityDoc) !== undefined
)
contexts = res.filter(
({ attachedToClass }) =>
hierarchy.classHierarchyMixin(attachedToClass, activity.mixin.ActivityDoc) !== undefined
)
}
},
{ sort: { createdOn: SortingOrder.Ascending } }
)
$: contexts = allContexts.filter(({ isPinned }) => !isPinned)
$: pinnedContexts = allContexts.filter(({ isPinned }) => isPinned)
$: loadObjects(contexts)
$: loadObjects(allContexts)
$: void getSections(objectsByClass, model).then((res) => {
sections = res
})
function loadObjects (allContexts: DocNotifyContext[]): void {
const contextsByClass = groupByArray(allContexts, ({ attachedToClass }) => attachedToClass)
function loadObjects (contexts: DocNotifyContext[]): void {
const contextsByClass = groupByArray(contexts, ({ attachedToClass }) => attachedToClass)
for (const [_class, contexts] of contextsByClass.entries()) {
const ids = contexts.map(({ attachedTo }) => attachedTo)
for (const [_class, ctx] of contextsByClass.entries()) {
const ids = ctx.map(({ attachedTo }) => attachedTo)
const query = objectsQueryByClass.get(_class) ?? createQuery()
objectsQueryByClass.set(_class, query)
@ -89,124 +92,60 @@
}
}
function archiveActivityChannels (contexts: DocNotifyContext[]): void {
showPopup(
MessageBox,
{
label: chunter.string.ArchiveActivityConfirmationTitle,
message: chunter.string.ArchiveActivityConfirmationMessage
},
'top',
(result?: boolean) => {
if (result === true) {
void removeActivityChannels(contexts)
}
}
)
}
async function getSections (
objectsByClass: Map<Ref<Class<Doc>>, Doc[]>,
model: ChatNavGroupModel
): Promise<Section[]> {
const result: Section[] = []
function getActions (contexts: DocNotifyContext[]): Action[] {
if (model.id !== 'activity') return []
if (!model.wrap) {
result.push({
id: model.id,
objects: Array.from(objectsByClass.values()).flat(),
label: await translate(model.label ?? chunter.string.Channels, {})
})
return [
{
icon: notification.icon.ReadAll,
label: notification.string.MarkReadAll,
action: () => readActivityChannels(contexts)
},
{
icon: view.icon.Archive,
label: notification.string.ArchiveAll,
action: async () => {
archiveActivityChannels(contexts)
}
}
]
}
function getPinnedActions (pinnedContexts: DocNotifyContext[]): Action[] {
const baseActions = getActions(pinnedContexts)
const actions: Action[] = [
{
icon: view.icon.Delete,
label: chunter.string.UnpinChannels,
action: chunter.actionImpl.UnpinAllChannels
}
].map(({ icon, label, action }) => ({
icon,
label,
action: async (_: any, evt: Event) => {
const actionFn = await getResource(action)
await actionFn(pinnedContexts, evt)
}
}))
return actions.concat(baseActions)
}
function sortContexts (contexts: DocNotifyContext[]): DocNotifyContext[] {
if (model.id !== 'activity') {
return contexts
return result
}
return contexts.sort((context1, context2) => {
const hasNewMessages1 = (context1.lastUpdateTimestamp ?? 0) > (context1.lastViewedTimestamp ?? 0)
const hasNewMessages2 = (context2.lastUpdateTimestamp ?? 0) > (context2.lastViewedTimestamp ?? 0)
if (hasNewMessages1 && hasNewMessages2) {
return (context2.lastUpdateTimestamp ?? 0) - (context1.lastUpdateTimestamp ?? 0)
}
for (const [_class, objects] of objectsByClass.entries()) {
const clazz = hierarchy.getClass(_class)
if (hasNewMessages1 && !hasNewMessages2) {
return -1
}
result.push({
id: _class,
_class,
objects,
label: await translate(clazz.pluralLabel ?? clazz.label, {})
})
}
if (hasNewMessages2 && !hasNewMessages1) {
return 1
}
return result.sort((s1, s2) => s1.label.localeCompare(s2.label))
}
return (context2.lastUpdateTimestamp ?? 0) - (context1.lastUpdateTimestamp ?? 0)
})
function getSectionActions (section: Section, contexts: DocNotifyContext[]): Action[] {
if (model.getActionsFn === undefined) {
return []
}
const { _class } = section
if (_class === undefined) {
return model.getActionsFn(contexts)
} else {
return model.getActionsFn(contexts.filter(({ attachedToClass }) => attachedToClass === _class))
}
}
</script>
<Scroller padding="0 0.5rem">
{#if pinnedContexts.length}
<div class="block">
<ChatGroupHeader header={chunter.string.Pinned} actions={getPinnedActions(pinnedContexts)} />
{#each pinnedContexts as context (context._id)}
{@const _class = context.attachedToClass}
{@const object = objectsByClass.get(_class)?.find(({ _id }) => _id === context.attachedTo)}
<ChatNavItem {context} isSelected={selectedContextId === context._id} doc={object} on:select />
{/each}
</div>
{/if}
{#if pinnedContexts.length > 0 && contexts.length}
<div class="separator" />
{/if}
{#if contexts.length}
<div class="block">
<ChatGroupHeader header={model.label} actions={getActions(contexts)} />
{#each contexts as context (context._id)}
{@const _class = context.attachedToClass}
{@const object = objectsByClass.get(_class)?.find(({ _id }) => _id === context.attachedTo)}
<ChatNavItem {context} isSelected={selectedContextId === context._id} doc={object} on:select />
{/each}
</div>
{/if}
</Scroller>
<style lang="scss">
.block {
display: flex;
flex-direction: column;
}
.separator {
width: 100%;
height: 1px;
background: var(--theme-navpanel-border);
margin-top: 0.75rem;
}
</style>
{#each sections as section (section.id)}
<ChatNavSection
objects={section.objects}
{contexts}
{selectedContextId}
header={section.label}
actions={getSectionActions(section, contexts)}
sortFn={model.sortFn}
maxItems={model.maxSectionItems}
on:select
/>
{/each}

View File

@ -13,66 +13,36 @@
// limitations under the License.
-->
<script lang="ts">
import type { Doc } from '@hcengineering/core'
import notification, { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import contact from '@hcengineering/contact'
import { Action, IconEdit, IconSize } from '@hcengineering/ui'
import { getActions, getDocTitle } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import { Action, IconEdit } from '@hcengineering/ui'
import { getActions } from '@hcengineering/view-resources'
import { getNotificationsCount, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { createEventDispatcher } from 'svelte'
import chunter from '../../../plugin'
import { getChannelIcon, getChannelName } from '../../../utils'
import Item from './NavItem.svelte'
import NavItem from './NavItem.svelte'
import { ChatNavItemModel } from '../types'
export let context: DocNotifyContext
export let doc: Doc | undefined = undefined
export let item: ChatNavItemModel
export let isSelected = false
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
const notificationClient = InboxNotificationsClientImpl.getClient()
let notifications: InboxNotification[] = []
let channelName: string | undefined = undefined
let description: string | undefined = undefined
let iconSize: IconSize = 'x-small'
let notificationsCount = 0
let actions: Action[] = []
$: doc &&
getChannelName(context.attachedTo, context.attachedToClass, doc).then((res) => {
channelName = res
})
$: doc &&
!hierarchy.isDerived(context.attachedToClass, chunter.class.ChunterSpace) &&
getDocTitle(client, context.attachedTo, context.attachedToClass, doc).then((res) => {
description = res
})
notificationClient.inboxNotificationsByContext.subscribe((res) => {
notifications = (res.get(context._id) ?? []).filter(
({ _class }) => _class === notification.class.ActivityInboxNotification
)
})
$: isDirect = hierarchy.isDerived(context.attachedToClass, chunter.class.DirectMessage)
$: isPerson = hierarchy.isDerived(context.attachedToClass, contact.class.Person)
$: isDocChat = !hierarchy.isDerived(context.attachedToClass, chunter.class.ChunterSpace)
$: if (isPerson) {
iconSize = 'medium'
} else if (isDocChat) {
iconSize = 'x-large'
} else {
iconSize = 'x-small'
}
$: void getNotificationsCount(context, notifications).then((res) => {
notificationsCount = res
})
@ -83,7 +53,11 @@
async function getChannelActions (context: DocNotifyContext): Promise<Action[]> {
const result = []
const excludedActions = [notification.action.DeleteContextNotifications, notification.action.UnReadNotifyContext]
const excludedActions = [
notification.action.DeleteContextNotifications,
notification.action.UnReadNotifyContext,
notification.action.ReadNotifyContext
]
const actions = (await getActions(client, context, notification.class.DocNotifyContext)).filter(
({ _id }) => !excludedActions.includes(_id)
)
@ -110,19 +84,19 @@
}
</script>
<Item
id={context._id}
icon={getChannelIcon(context.attachedToClass)}
withIconBackground={!isDirect && !isPerson}
{iconSize}
isBold={isDocChat}
<NavItem
id={item.id}
icon={item.icon}
withIconBackground={item.withIconBackground}
isSecondary={item.isSecondary}
iconSize={item.iconSize}
{isSelected}
iconProps={{ value: doc }}
iconProps={{ value: item.object }}
{notificationsCount}
title={channelName}
{description}
title={item.title}
description={item.description}
{actions}
on:click={() => {
dispatch('select', { doc, context })
dispatch('select', { doc: item.object, context })
}}
/>

View File

@ -0,0 +1,176 @@
<!--
// 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 { Doc, Ref } from '@hcengineering/core'
import { DocNotifyContext } from '@hcengineering/notification'
import { getClient } from '@hcengineering/presentation'
import ui, { Action, IconSize, ModernButton } from '@hcengineering/ui'
import { getDocTitle } from '@hcengineering/view-resources'
import contact from '@hcengineering/contact'
import { translate } from '@hcengineering/platform'
import ChatNavItem from './ChatNavItem.svelte'
import chunter from '../../../plugin'
import { ChatNavItemModel } from '../types'
import { getChannelIcon, getChannelName } from '../../../utils'
import ChatSectionHeader from './ChatSectionHeader.svelte'
export let header: string
export let objects: Doc[]
export let contexts: DocNotifyContext[]
export let actions: Action[] = []
export let maxItems: number | undefined = undefined
export let selectedContextId: Ref<DocNotifyContext> | undefined = undefined
export let sortFn: (items: ChatNavItemModel[], contexts: DocNotifyContext[]) => ChatNavItemModel[]
const client = getClient()
const hierarchy = client.getHierarchy()
let items: ChatNavItemModel[] = []
let visibleItems: ChatNavItemModel[] = []
let isCollapsed = false
let canShowMore = false
let isShownMore = false
$: void getChatNavItems(objects).then((res) => {
items = sortFn(res, contexts)
})
$: canShowMore = !!maxItems && items.length > maxItems
$: visibleItems = getVisibleItems(canShowMore, isShownMore, maxItems, items, selectedContextId, contexts)
async function getChatNavItems (objects: Doc[]): Promise<ChatNavItemModel[]> {
const items: ChatNavItemModel[] = []
for (const object of objects) {
const { _class } = object
const icon = getChannelIcon(_class)
const titleIntl = client.getHierarchy().getClass(_class).label
const isPerson = hierarchy.isDerived(_class, contact.class.Person)
const isDocChat = !hierarchy.isDerived(_class, chunter.class.ChunterSpace)
const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage)
const iconSize: IconSize = isDirect || isPerson ? 'x-small' : 'small'
items.push({
id: object._id,
object,
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
description: isDocChat && !isPerson ? await getDocTitle(client, object._id, object._class, object) : undefined,
icon,
iconSize,
withIconBackground: !isDirect && !isPerson,
isSecondary: isDocChat && !isPerson
})
}
return items
}
function onShowMore (): void {
isShownMore = !isShownMore
}
function getVisibleItems (
canShowMore: boolean,
isShownMore: boolean,
maxItems: number | undefined,
items: ChatNavItemModel[],
selectedContextId: Ref<DocNotifyContext> | undefined,
contexts: DocNotifyContext[]
): ChatNavItemModel[] {
if (!canShowMore || isShownMore) {
return items
}
const result = items.slice(0, maxItems)
if (selectedContextId === undefined) {
return result
}
const context = contexts.find(({ _id }) => _id === selectedContextId)
if (context === undefined) {
return result
}
const exists = result.some(({ id }) => id === context.attachedTo)
if (exists) {
return result
}
const selectedItem = items.find(({ id }) => id === context?.attachedTo)
if (selectedItem === undefined) {
return result
}
result.push(selectedItem)
return result
}
</script>
{#if items.length > 0 && contexts.length > 0}
<div class="section">
<ChatSectionHeader
{header}
{actions}
{isCollapsed}
on:collapse={() => {
isCollapsed = !isCollapsed
}}
/>
{#if !isCollapsed}
{#each visibleItems as item (item.id)}
{@const context = contexts.find(({ attachedTo }) => attachedTo === item.id)}
{#if context}
<ChatNavItem {context} isSelected={selectedContextId === context._id} {item} on:select />
{/if}
{/each}
{#if canShowMore}
<div class="showMore">
<ModernButton
label={isShownMore ? ui.string.ShowLess : ui.string.ShowMore}
kind="tertiary"
inheritFont
size="extra-small"
on:click={onShowMore}
/>
</div>
{/if}
{/if}
</div>
{/if}
<style lang="scss">
.section {
display: flex;
gap: 0.125rem;
flex-direction: column;
padding: 0 var(--spacing-1) var(--spacing-1_5) var(--spacing-1);
border-bottom: 1px solid var(--global-surface-02-BorderColor);
}
.showMore {
margin-top: var(--spacing-1);
font-size: 0.75rem;
}
</style>

View File

@ -13,61 +13,40 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Ref } from '@hcengineering/core'
import {
getCurrentLocation,
IModeSelector,
ModeSelector,
navigate,
location as locationStore,
Scroller,
SearchEdit,
Label,
Button,
IconAdd,
showPopup,
Menu,
Action
} from '@hcengineering/ui'
import { DocNotifyContext, InboxNotification } from '@hcengineering/notification'
import { Ref } from '@hcengineering/core'
import { Scroller, SearchEdit, Label, Button, IconAdd, showPopup, Menu } from '@hcengineering/ui'
import { DocNotifyContext } 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 { getNotificationsCount, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { getClient } from '@hcengineering/presentation'
import activity from '@hcengineering/activity'
import { getResource } from '@hcengineering/platform'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import chunter from '../../../plugin'
import ChatNavGroup from './ChatNavGroup.svelte'
import { Mode } from '../types'
import { chatNavGroupsModel, chatSpecials } from '../utils'
import { chatNavGroupModels, chatSpecials } from '../utils'
import ChatSpecialElement from './ChatSpecialElement.svelte'
import { userSearch } from '../../../index'
import { navigateToSpecial } from '../../../utils'
export let selectedContextId: Ref<DocNotifyContext> | undefined
export let selectedObjectClass: Ref<Class<DocNotifyContext>> | undefined
export let currentSpecial: SpecialNavModel | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const notificationClient = InboxNotificationsClientImpl.getClient()
const contextsStore = notificationClient.docNotifyContexts
const notificationsByContextStore = notificationClient.inboxNotificationsByContext
const actions = [
const globalActions = [
{
label: chunter.string.NewChannel,
icon: chunter.icon.Hashtag,
action: async (_id: Ref<Doc>): Promise<void> => {
action: async (): Promise<void> => {
showPopup(chunter.component.CreateChannel, {}, 'top')
}
},
{
label: chunter.string.NewDirectChat,
icon: chunter.icon.Thread,
action: async (_id: Ref<Doc>): Promise<void> => {
action: async (): Promise<void> => {
showPopup(chunter.component.CreateDirectChat, {}, 'top')
}
}
@ -75,68 +54,7 @@
const searchValue: string = ''
const modesConfig: Array<[Mode, IntlString, object]> = chatNavGroupsModel.map(({ id, tabLabel }) => [
id,
tabLabel,
{}
])
let modeSelectorProps: IModeSelector
let mode: Mode | undefined
let notifyModes: Mode[] = []
$: mode = ($locationStore.query?.mode ?? undefined) as Mode | undefined
$: if (mode === undefined) {
;[[mode]] = modesConfig
}
$: modeSelectorProps = {
mode: (mode ?? modesConfig[0][0]) as string,
config: modesConfig,
onChange: (mode: string) => {
handleModeChanged(mode as Mode)
}
}
$: getModesWithNotifications($contextsStore, $notificationsByContextStore).then((res) => {
notifyModes = res
})
$: updateSelectedContextMode(selectedObjectClass)
$: model = chatNavGroupsModel.find(({ id }) => id === mode) ?? chatNavGroupsModel[0]
function updateSelectedContextMode (objectClass?: Ref<Class<Doc>>) {
if (objectClass === undefined) {
return
}
if (hierarchy.isDerived(objectClass, chunter.class.Channel)) {
if (mode !== 'channels') {
handleModeChanged('channels')
modeSelectorProps.mode = 'channels'
}
} else if (hierarchy.isDerived(objectClass, chunter.class.DirectMessage)) {
if (mode !== 'direct') {
handleModeChanged('direct')
modeSelectorProps.mode = 'direct'
}
} else if (mode !== 'activity') {
handleModeChanged('activity')
modeSelectorProps.mode = 'activity'
}
}
function handleModeChanged (newMode: Mode) {
const loc = getCurrentLocation()
mode = newMode
loc.query = { ...loc.query, mode }
navigate(loc)
}
async function isSpecialVisible (special: SpecialNavModel, docNotifyContexts: DocNotifyContext[]) {
async function isSpecialVisible (special: SpecialNavModel, docNotifyContexts: DocNotifyContext[]): Promise<boolean> {
if (special.visibleIf === undefined) {
return true
}
@ -146,51 +64,8 @@
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
function addButtonClicked (ev: MouseEvent): void {
showPopup(Menu, { actions: globalActions }, ev.target as HTMLElement)
}
</script>
@ -228,16 +103,12 @@
}}
/>
</div>
<ModeSelector
props={modeSelectorProps}
kind="separated-free"
padding="0"
expansion="stretch"
notifyFor={notifyModes}
/>
<ChatNavGroup {selectedContextId} {model} on:select />
<div class="antiNav-space" />
<Scroller>
{#each chatNavGroupModels as model}
<ChatNavGroup {selectedContextId} {model} on:select />
{/each}
<div class="antiNav-space" />
</Scroller>
</Scroller>
<style lang="scss">
@ -249,9 +120,10 @@
margin-left: 1.25rem;
font-weight: 700;
font-size: 1.25rem;
color: var(--theme-content-color);
color: var(--global-primary-TextColor);
}
.search {
padding: 12px;
padding: var(--spacing-1_5);
border-bottom: 1px solid var(--global-surface-02-BorderColor);
}
</style>

View File

@ -0,0 +1,77 @@
<!--
// 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 { Action, ButtonIcon, IconDropdownDown, IconDropdownRight, Menu, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let header: string
export let actions: Action[] = []
export let isCollapsed = false
const dispatch = createEventDispatcher()
function handleMenuClicked (ev: MouseEvent): void {
if (actions.length === 0) {
return
}
showPopup(Menu, { actions }, ev.target as HTMLElement)
}
</script>
<div class="root">
<ButtonIcon
size="extra-small"
kind="tertiary"
inheritColor
icon={isCollapsed ? IconDropdownRight : IconDropdownDown}
on:click={() => dispatch('collapse')}
/>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="header uppercase" class:disabled={actions.length === 0} on:click={handleMenuClicked}>
{header}
</div>
</div>
<style lang="scss">
.root {
display: flex;
align-items: center;
flex-shrink: 0;
border-radius: 0.375rem;
padding-right: 0.25rem;
color: var(--global-secondary-TextColor);
font-size: 0.75rem;
}
.header {
cursor: pointer;
padding: var(--spacing-0_5) var(--spacing-0_75);
border-radius: var(--extra-small-BorderRadius);
background-color: var(--global-ui-BackgroundColor);
width: fit-content;
margin: 0.5rem 0.25rem;
&.disabled {
cursor: default;
}
&:hover:not(.disabled) {
color: var(--global-primary-TextColor);
background-color: var(--global-ui-highlight-BackgroundColor);
}
}
</style>

View File

@ -34,7 +34,7 @@
let notificationsCount = 0
let elementsCount = 0
$: getNotificationsCount(special, $notificationsByContextStore).then((res) => {
$: void getNotificationsCount(special, $notificationsByContextStore).then((res) => {
notificationsCount = res
})
$: elementsCount = getElementsCount(special, $savedMessagesStore, $savedAttachmentsStore)
@ -42,7 +42,7 @@
async function getNotificationsCount (
special: SpecialNavModel,
notificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>
) {
): Promise<number> {
if (!special.notificationsCountProvider) {
return 0
}
@ -71,6 +71,7 @@
icon={special.icon}
iconPadding="0 0 0 0.375rem"
iconSize="small"
padding="var(--spacing-1) var(--spacing-0_5)"
intlTitle={special.label}
withIconBackground={false}
{notificationsCount}

View File

@ -33,9 +33,10 @@
export let iconProps: any | undefined = undefined
export let iconSize: IconSize = 'x-small'
export let iconPadding: string | null = null
export let padding: string | null = null
export let withIconBackground = true
export let isSelected = false
export let isBold = false
export let isSecondary = false
export let notificationsCount = 0
export let title: string | undefined = undefined
export let intlTitle: IntlString | undefined = undefined
@ -50,21 +51,21 @@
$: inlineActions = actions.filter(({ inline }) => inline === true)
$: menuActions = actions.filter(({ inline }) => inline !== true)
async function handleMenuClicked (ev: MouseEvent) {
function handleMenuClicked (ev: MouseEvent): void {
showPopup(Menu, { actions: menuActions, ctx: id }, ev.target as HTMLElement, () => {
menuOpened = false
})
menuOpened = true
}
async function handleInlineActionClicked (ev: MouseEvent, action: Action) {
async function handleInlineActionClicked (ev: MouseEvent, action: Action): Promise<void> {
await action.action([], ev)
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="root" class:pressed={menuOpened || isSelected} on:click>
<div class="root" class:pressed={menuOpened || isSelected} style:padding on:click>
{#if icon}
<div class="icon" class:withBackground={withIconBackground} style:padding={iconPadding}>
<Icon
@ -79,8 +80,8 @@
<div class="content">
<span
class="label overflow-label"
class:bold={isBold}
class:extraBold={notificationsCount > 0 || isSelected}
class:secondary={isSecondary}
class:extraBold={notificationsCount > 0}
class:selected={isSelected}
style="flex-shrink: 0"
>
@ -89,13 +90,12 @@
{:else if intlTitle}
<Label label={intlTitle} />
{/if}
{#if description}
<span class="label overflow-label ml-1-5" title={description}>
{description}
</span>
{/if}
</span>
{#if description}
<span class="label secondary overflow-label withWrap mt-0-5" title={description}>
{description}
</span>
{/if}
</div>
<div class="grower" />
@ -137,8 +137,8 @@
display: flex;
align-items: center;
flex-shrink: 0;
padding: 0.5rem 0.25rem 0.5rem 0.25rem;
border-radius: 0.375rem;
padding: var(--spacing-0_5) var(--spacing-0_5);
border-radius: var(--small-BorderRadius);
cursor: pointer;
position: relative;
@ -187,10 +187,10 @@
color: var(--global-primary-TextColor);
&.withBackground {
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;
background: var(--global-ui-BackgroundColor);
width: 1.5rem;
height: 1.5rem;
border-radius: var(--extra-small-BorderRadius);
border: 1px solid var(--global-subtle-ui-BorderColor);
}
}
@ -201,10 +201,7 @@
font-weight: 400;
&.secondary {
color: var(--global-secondary-TextColor);
}
&.bold {
font-size: 0.75rem;
font-weight: 500;
}
@ -212,18 +209,9 @@
font-weight: 700;
}
&.withWrap {
display: -webkit-box;
/* autoprefixer: ignore next */
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
white-space: break-spaces;
word-break: break-word;
}
&.selected {
color: var(--global-primary-LinkColor);
color: var(--global-accent-TextColor);
font-weight: 500;
}
}

View File

@ -12,15 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type IntlString } from '@hcengineering/platform'
import { type DocumentQuery } from '@hcengineering/core'
import { type Asset, type IntlString } from '@hcengineering/platform'
import { type Doc, type DocumentQuery, type Ref } from '@hcengineering/core'
import { type DocNotifyContext } from '@hcengineering/notification'
export type Mode = 'channels' | 'direct' | 'activity'
import { type AnySvelteComponent, type IconSize, type Action } from '@hcengineering/ui'
export interface ChatNavGroupModel {
id: Mode
label: IntlString
tabLabel: IntlString
id: string
label?: IntlString
query: DocumentQuery<DocNotifyContext>
sortFn: (items: ChatNavItemModel[], contexts: DocNotifyContext[]) => ChatNavItemModel[]
wrap: boolean
getActionsFn?: (contexts: DocNotifyContext[]) => Action[]
maxSectionItems?: number
}
export interface ChatNavItemModel {
id: Ref<Doc>
object: Doc
title: string
description?: string
icon: Asset | AnySvelteComponent | undefined
iconSize?: IconSize
isSecondary: boolean
withIconBackground: boolean
}

View File

@ -14,15 +14,16 @@
//
import notification, { type DocNotifyContext } from '@hcengineering/notification'
import { generateId, SortingOrder, type WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import { get, writable } from 'svelte/store'
import view from '@hcengineering/view'
import workbench, { type SpecialNavModel } from '@hcengineering/workbench'
import { type SpecialNavModel } from '@hcengineering/workbench'
import attachment, { type SavedAttachments } from '@hcengineering/attachment'
import activity from '@hcengineering/activity'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { type Action, showPopup } from '@hcengineering/ui'
import { type ChatNavGroupModel } from './types'
import { type ChatNavGroupModel, type ChatNavItemModel } from './types'
import chunter from '../../plugin'
export const savedAttachmentsStore = writable<Array<WithLookup<SavedAttachments>>>([])
@ -49,52 +50,70 @@ export const chatSpecials: SpecialNavModel[] = [
icon: view.icon.Database,
component: chunter.component.ChunterBrowser,
position: 'top'
},
{
id: 'archive',
component: workbench.component.Archive,
icon: view.icon.Archive,
label: workbench.string.Archive,
position: 'top',
componentProps: {
_class: notification.class.DocNotifyContext,
config: [
{ key: '', label: chunter.string.ChannelName },
{ key: 'attachedToClass', label: view.string.Type },
'modifiedOn'
],
baseMenuClass: notification.class.DocNotifyContext,
query: {
_class: notification.class.DocNotifyContext,
hidden: true
}
},
visibleIf: notification.function.HasHiddenDocNotifyContext
}
// TODO: Should be reworked or removed
// {
// id: 'archive',
// component: workbench.component.Archive,
// icon: view.icon.Archive,
// label: workbench.string.Archive,
// position: 'top',
// componentProps: {
// _class: notification.class.DocNotifyContext,
// config: [
// { key: '', label: chunter.string.ChannelName },
// { key: 'attachedToClass', label: view.string.Type },
// 'modifiedOn'
// ],
// baseMenuClass: notification.class.DocNotifyContext,
// query: {
// _class: notification.class.DocNotifyContext,
// hidden: true
// }
// },
// visibleIf: notification.function.HasHiddenDocNotifyContext
// }
]
export const chatNavGroupsModel: ChatNavGroupModel[] = [
export const chatNavGroupModels: ChatNavGroupModel[] = [
{
id: 'starred',
label: chunter.string.Starred,
sortFn: sortAlphabetically,
wrap: false,
getActionsFn: getPinnedActions,
query: {
isPinned: true
}
},
{
id: 'channels',
tabLabel: chunter.string.Channels,
label: chunter.string.AllChannels,
sortFn: sortAlphabetically,
wrap: true,
getActionsFn: getChannelsActions,
query: {
isPinned: { $ne: true },
attachedToClass: { $in: [chunter.class.Channel] }
}
},
{
id: 'direct',
tabLabel: chunter.string.Direct,
label: chunter.string.AllContacts,
sortFn: sortAlphabetically,
wrap: true,
getActionsFn: getDirectActions,
query: {
isPinned: { $ne: true },
attachedToClass: { $in: [chunter.class.DirectMessage] }
}
},
{
id: 'activity',
tabLabel: activity.string.Activity,
label: activity.string.Activity,
sortFn: sortActivityChannels,
wrap: true,
getActionsFn: getActivityActions,
maxSectionItems: 5,
query: {
isPinned: { $ne: true },
attachedToClass: {
$nin: [chunter.class.DirectMessage, chunter.class.Channel]
}
@ -102,6 +121,125 @@ export const chatNavGroupsModel: ChatNavGroupModel[] = [
}
]
function sortAlphabetically (items: ChatNavItemModel[]): ChatNavItemModel[] {
return items.sort((i1, i2) => i1.title.localeCompare(i2.title))
}
function sortActivityChannels (items: ChatNavItemModel[], contexts: DocNotifyContext[]): ChatNavItemModel[] {
const contextByDoc = new Map(contexts.map((context) => [context.attachedTo, context]))
return items.sort((i1, i2) => {
const context1 = contextByDoc.get(i1.id)
const context2 = contextByDoc.get(i2.id)
if (context1 === undefined || context2 === undefined) {
return 1
}
const hasNewMessages1 = (context1.lastUpdateTimestamp ?? 0) > (context1.lastViewedTimestamp ?? 0)
const hasNewMessages2 = (context2.lastUpdateTimestamp ?? 0) > (context2.lastViewedTimestamp ?? 0)
if (hasNewMessages1 && hasNewMessages2) {
return (context2.lastUpdateTimestamp ?? 0) - (context1.lastUpdateTimestamp ?? 0)
}
if (hasNewMessages1 && !hasNewMessages2) {
return -1
}
if (hasNewMessages2 && !hasNewMessages1) {
return 1
}
return (context2.lastUpdateTimestamp ?? 0) - (context1.lastUpdateTimestamp ?? 0)
})
}
function getPinnedActions (contexts: DocNotifyContext[]): Action[] {
return [
{
icon: view.icon.Delete,
label: chunter.string.DeleteStarred,
action: async () => {
await unpinAllChannels(contexts)
}
}
]
}
async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
const doneOp = await getClient().measure('unpinAllChannels')
const ops = getClient().apply(generateId())
try {
for (const context of contexts) {
await ops.update(context, { isPinned: false })
}
} finally {
await ops.commit()
await doneOp()
}
}
function getChannelsActions (): Action[] {
return [
{
icon: chunter.icon.Hashtag,
label: chunter.string.CreateChannel,
action: async (): Promise<void> => {
showPopup(chunter.component.CreateChannel, {}, 'top')
}
}
]
}
function getDirectActions (): Action[] {
return [
{
label: chunter.string.NewDirectChat,
icon: chunter.icon.Thread,
action: async (): Promise<void> => {
showPopup(chunter.component.CreateDirectChat, {}, 'top')
}
}
]
}
function getActivityActions (contexts: DocNotifyContext[]): Action[] {
return [
{
icon: notification.icon.ReadAll,
label: notification.string.MarkReadAll,
action: async () => {
await readActivityChannels(contexts)
}
},
{
icon: view.icon.Archive,
label: notification.string.ArchiveAll,
action: async () => {
archiveActivityChannels(contexts)
}
}
]
}
function archiveActivityChannels (contexts: DocNotifyContext[]): void {
showPopup(
MessageBox,
{
label: chunter.string.ArchiveActivityConfirmationTitle,
message: chunter.string.ArchiveActivityConfirmationMessage
},
'top',
(result?: boolean) => {
if (result === true) {
void removeActivityChannels(contexts)
}
}
)
}
export function loadSavedAttachments (): void {
const client = getClient()

View File

@ -66,10 +66,11 @@ import {
getUnreadThreadsCount,
canCopyMessageLink,
buildThreadLink,
getThreadLink
getThreadLink,
leaveChannelAction,
removeChannelAction
} 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'
@ -129,10 +130,10 @@ async function ConvertDmToPrivateChannel (dm: DirectMessage): Promise<void> {
})
}
async function OpenChannel (
export async function openChannel (
notifyContext?: DocNotifyContext,
evt?: Event,
props?: { mode?: Mode, _id: Ref<DocNotifyContext> }
props?: { _id: Ref<DocNotifyContext> }
): Promise<void> {
evt?.preventDefault()
@ -151,18 +152,12 @@ async function OpenChannel (
loc.path[3] = id
loc.path.length = 4
loc.query = { mode: props?.mode ?? loc.query?.mode ?? null, message: null }
loc.query = { message: null }
loc.fragment = undefined
navigate(loc)
}
async function UnpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
const client = getClient()
await Promise.all(contexts.map(async (context) => await client.update(context, { isPinned: false })))
}
export const userSearch = writable('')
export async function chunterBrowserVisible (): Promise<boolean> {
@ -263,7 +258,8 @@ export default async (): Promise<Resources> => ({
UnarchiveChannel,
ConvertDmToPrivateChannel,
DeleteChatMessage: deleteChatMessage,
OpenChannel,
UnpinAllChannels
OpenChannel: openChannel,
LeaveChannel: leaveChannelAction,
RemoveChannel: removeChannelAction
}
})

View File

@ -54,8 +54,9 @@ export default mergeIds(chunterId, chunter, {
UnsubscribeMessage: '' as ViewAction,
SubscribeComment: '' as ViewAction,
UnsubscribeComment: '' as ViewAction,
UnpinAllChannels: '' as ViewAction,
OpenChannel: '' as ViewAction
OpenChannel: '' as ViewAction,
LeaveChannel: '' as ViewAction,
RemoveChannel: '' as ViewAction
},
string: {
Channel: '' as IntlString,

View File

@ -52,7 +52,7 @@ import activity, {
type DisplayDocUpdateMessage,
type DocUpdateMessage
} from '@hcengineering/activity'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { deleteContextNotifications, InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import notification, { type DocNotifyContext, notificationId } from '@hcengineering/notification'
import { get, type Unsubscriber } from 'svelte/store'
@ -451,7 +451,10 @@ export async function leaveChannel (channel: Channel, value: Ref<Account> | Arra
await client.update(channel, { $pull: { members: { $in: value } } })
}
} else {
const context = await client.findOne(notification.class.DocNotifyContext, { attachedTo: channel._id })
await client.update(channel, { $pull: { members: value } })
await removeChannelAction(context)
}
}
@ -493,3 +496,28 @@ export async function readChannelMessages (
void client.update(context, { lastViewedTimestamp: lastTimestamp })
}
}
export async function leaveChannelAction (context?: DocNotifyContext): Promise<void> {
if (context === undefined) {
return
}
const client = getClient()
const channel = await client.findOne(chunter.class.Channel, { _id: context.attachedTo as Ref<Channel> })
if (channel === undefined) {
return
}
await leaveChannel(channel, getCurrentAccount()._id)
}
export async function removeChannelAction (context?: DocNotifyContext): Promise<void> {
if (context === undefined) {
return
}
const client = getClient()
await deleteContextNotifications(context)
await client.remove(context)
}

View File

@ -190,7 +190,14 @@ export default plugin(chunterId, {
Public: '' as IntlString,
Private: '' as IntlString,
NewDirectChat: '' as IntlString,
AddMembers: '' as IntlString
AddMembers: '' as IntlString,
CloseConversation: '' as IntlString,
Starred: '' as IntlString,
DeleteStarred: '' as IntlString,
StarChannel: '' as IntlString,
StarConversation: '' as IntlString,
UnstarChannel: '' as IntlString,
UnstarConversation: '' as IntlString
},
ids: {
DMNotification: '' as Ref<NotificationType>,
@ -203,6 +210,9 @@ export default plugin(chunterId, {
},
action: {
DeleteChatMessage: '' as Ref<Action>,
OpenChannel: '' as Ref<Action>
OpenChannel: '' as Ref<Action>,
LeaveChannel: '' as Ref<Action>,
RemoveChannel: '' as Ref<Action>,
CloseConversation: '' as Ref<Action>
}
})

View File

@ -22,5 +22,5 @@
</script>
{#if value}
<Avatar avatar={value.avatar} {size} />
<Avatar avatar={value.avatar} {size} name={value.name} />
{/if}

View File

@ -40,6 +40,8 @@
"MarkReadAll": "Mark all as read",
"MarkUnreadAll": "Mark all as unread",
"ArchiveAllConfirmationTitle": "Archive all notifications?",
"ArchiveAllConfirmationMessage": "Are you sure you want to archive all notifications? This operation cannot be undone."
"ArchiveAllConfirmationMessage": "Are you sure you want to archive all notifications? This operation cannot be undone.",
"StarDocument": "Star document",
"UnstarDocument": "Unstar document"
}
}

View File

@ -40,6 +40,8 @@
"MarkReadAll": "Oтметить все как прочитанное",
"MarkUnreadAll": "Отметить все как непрочитанные",
"ArchiveAllConfirmationTitle": "Архивировать все уведомления?",
"ArchiveAllConfirmationMessage": "Вы уверены, что хотите заархивировать все уведомления? Эту операцию невозможно отменить."
"ArchiveAllConfirmationMessage": "Вы уверены, что хотите заархивировать все уведомления? Эту операцию невозможно отменить.",
"StarDocument": "Добавить в избранное",
"UnstarDocument": "Удалить из избранного"
}
}

View File

@ -103,4 +103,7 @@
<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>
<symbol id="star" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 2.00012C16.3771 2.00012 16.7207 2.22003 16.8829 2.56529L20.6834 10.6536L29.1675 11.9485C29.5317 12.0041 29.8342 12.2625 29.9502 12.6169C30.0662 12.9714 29.976 13.3619 29.7168 13.6273L23.5575 19.9334L25.0129 28.8463C25.074 29.2203 24.9195 29.5969 24.6148 29.8166C24.3101 30.0363 23.9086 30.0607 23.5803 29.8794L16 25.6935L8.41968 29.8794C8.09136 30.0607 7.68986 30.0363 7.3852 29.8166C7.08055 29.5969 6.92604 29.2203 6.98711 28.8463L8.44251 19.9334L2.28319 13.6273C2.02402 13.3619 1.93381 12.9714 2.04979 12.6169C2.16578 12.2625 2.4683 12.0041 2.83248 11.9485L11.3166 10.6536L15.1171 2.56529C15.2793 2.22003 15.6229 2.00012 16 2.00012ZM16 5.29761L12.8589 11.9827C12.7191 12.2801 12.4428 12.4877 12.1215 12.5368L5.02857 13.6193L10.1839 18.8975C10.4047 19.1236 10.5052 19.4435 10.4539 19.7575L9.24424 27.1654L15.5323 23.6931C15.8239 23.5321 16.1761 23.5321 16.4677 23.6931L22.7558 27.1654L21.5461 19.7575C21.4948 19.4435 21.5953 19.1236 21.8161 18.8975L26.9714 13.6193L19.8785 12.5368C19.5572 12.4877 19.2809 12.2801 19.1411 11.9827L16 5.29761Z" />
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -40,5 +40,6 @@ loadMetadata(view.icon, {
ViewButton: `${icons}#viewButton`,
Filter: `${icons}#filter`,
Configure: `${icons}#configure`,
Database: `${icons}#database`
Database: `${icons}#database`,
Star: `${icons}#star`
})

View File

@ -217,7 +217,8 @@ const view = plugin(viewId, {
ViewButton: '' as Asset,
Filter: '' as Asset,
Configure: '' as Asset,
Database: '' as Asset
Database: '' as Asset,
Star: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,