mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Update chat navigator (#4941)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
cadde5a4b5
commit
fb6410fc74
@ -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 }
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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>,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -188,6 +188,7 @@ export interface Class<T extends Obj> extends Classifier {
|
||||
shortLabel?: string
|
||||
sortingKey?: string
|
||||
filteringKey?: string
|
||||
pluralLabel?: IntlString
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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); }
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
24
packages/ui/src/components/icons/DropdownDown.svelte
Normal file
24
packages/ui/src/components/icons/DropdownDown.svelte
Normal 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>
|
24
packages/ui/src/components/icons/DropdownRight.svelte
Normal file
24
packages/ui/src/components/icons/DropdownRight.svelte
Normal 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>
|
@ -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'
|
||||
|
@ -122,7 +122,6 @@ export interface TabItem {
|
||||
icon?: Asset | AnySvelteComponent
|
||||
color?: string
|
||||
tooltip?: IntlString
|
||||
showNotify?: boolean
|
||||
action?: () => void
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -98,6 +98,13 @@
|
||||
"LoadingHistory": "Загрузка истории...",
|
||||
"UnpinChannels": "Открепить все каналы",
|
||||
"ArchiveActivityConfirmationTitle": "Архивировать все каналы активности?",
|
||||
"ArchiveActivityConfirmationMessage": "Вы уверены, что хотите заархивировать все каналы активности? Эту операцию невозможно отменить."
|
||||
"ArchiveActivityConfirmationMessage": "Вы уверены, что хотите заархивировать все каналы активности? Эту операцию невозможно отменить.",
|
||||
"CloseConversation": "Закрыть диалог",
|
||||
"Starred": "Избранное",
|
||||
"DeleteStarred": "Удалить избранное",
|
||||
"StarChannel": "Добавить в избранное",
|
||||
"StarConversation": "Добавить в избранное",
|
||||
"UnstarChannel": "Удалить из избранного",
|
||||
"UnstarConversation": "Удалить из избранного"
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -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>
|
@ -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}
|
||||
|
@ -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 })
|
||||
}}
|
||||
/>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
})
|
||||
|
@ -22,5 +22,5 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<Avatar avatar={value.avatar} {size} />
|
||||
<Avatar avatar={value.avatar} {size} name={value.name} />
|
||||
{/if}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,8 @@
|
||||
"MarkReadAll": "Oтметить все как прочитанное",
|
||||
"MarkUnreadAll": "Отметить все как непрочитанные",
|
||||
"ArchiveAllConfirmationTitle": "Архивировать все уведомления?",
|
||||
"ArchiveAllConfirmationMessage": "Вы уверены, что хотите заархивировать все уведомления? Эту операцию невозможно отменить."
|
||||
"ArchiveAllConfirmationMessage": "Вы уверены, что хотите заархивировать все уведомления? Эту операцию невозможно отменить.",
|
||||
"StarDocument": "Добавить в избранное",
|
||||
"UnstarDocument": "Удалить из избранного"
|
||||
}
|
||||
}
|
||||
|
@ -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 |
@ -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`
|
||||
})
|
||||
|
@ -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>,
|
||||
|
Loading…
Reference in New Issue
Block a user