Notification (#934)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-02-07 15:03:14 +06:00 committed by GitHub
parent bf2e0aba30
commit e026683db0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 847 additions and 140 deletions

View File

@ -56,6 +56,7 @@ specifiers:
'@rush-temp/model-gmail': file:./projects/model-gmail.tgz
'@rush-temp/model-inventory': file:./projects/model-inventory.tgz
'@rush-temp/model-lead': file:./projects/model-lead.tgz
'@rush-temp/model-notification': file:./projects/model-notification.tgz
'@rush-temp/model-presentation': file:./projects/model-presentation.tgz
'@rush-temp/model-recruit': file:./projects/model-recruit.tgz
'@rush-temp/model-rig': file:./projects/model-rig.tgz
@ -70,6 +71,8 @@ specifiers:
'@rush-temp/model-view': file:./projects/model-view.tgz
'@rush-temp/model-workbench': file:./projects/model-workbench.tgz
'@rush-temp/mongo': file:./projects/mongo.tgz
'@rush-temp/notification': file:./projects/notification.tgz
'@rush-temp/notification-resources': file:./projects/notification-resources.tgz
'@rush-temp/panel': file:./projects/panel.tgz
'@rush-temp/platform': file:./projects/platform.tgz
'@rush-temp/platform-rig': file:./projects/platform-rig.tgz
@ -251,6 +254,7 @@ dependencies:
'@rush-temp/model-gmail': file:projects/model-gmail.tgz_typescript@4.5.4
'@rush-temp/model-inventory': file:projects/model-inventory.tgz_typescript@4.5.4
'@rush-temp/model-lead': file:projects/model-lead.tgz_typescript@4.5.4
'@rush-temp/model-notification': file:projects/model-notification.tgz_typescript@4.5.4
'@rush-temp/model-presentation': file:projects/model-presentation.tgz_typescript@4.5.4
'@rush-temp/model-recruit': file:projects/model-recruit.tgz_typescript@4.5.4
'@rush-temp/model-rig': file:projects/model-rig.tgz_37f79b97d0d86442e45d380c86f520c5
@ -265,6 +269,8 @@ dependencies:
'@rush-temp/model-view': file:projects/model-view.tgz_typescript@4.5.4
'@rush-temp/model-workbench': file:projects/model-workbench.tgz_typescript@4.5.4
'@rush-temp/mongo': file:projects/mongo.tgz
'@rush-temp/notification': file:projects/notification.tgz
'@rush-temp/notification-resources': file:projects/notification-resources.tgz_ac194b5590200ebf8338e0f86ec190f4
'@rush-temp/panel': file:projects/panel.tgz_096c09b0b673a57c275d9767a12070b1
'@rush-temp/platform': file:projects/platform.tgz
'@rush-temp/platform-rig': file:projects/platform-rig.tgz_37f79b97d0d86442e45d380c86f520c5
@ -12094,7 +12100,7 @@ packages:
dev: false
file:projects/gmail-resources.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-zZWhkD/HZH3/ljH7WhD2IG9NhQ6b1JPTgG2Rc35pGwScC2uzyI8xT3cq4K/jbsYLZ3BXj31BYOno7Mqo6pFcZg==, tarball: file:projects/gmail-resources.tgz}
resolution: {integrity: sha512-C7jgVQmN2v6HBKZgVAzWPYRaWrRHhfWpIRIRUyBUmspQreX2ArmCFCjq+1CHYBh9lGcnBZGgaNN1zQ1aTzSFLA==, tarball: file:projects/gmail-resources.tgz}
id: file:projects/gmail-resources.tgz
name: '@rush-temp/gmail-resources'
version: 0.0.0
@ -12425,7 +12431,7 @@ packages:
dev: false
file:projects/model-all.tgz_typescript@4.5.4:
resolution: {integrity: sha512-KRaYm65Scg01RukljQmwVKLg143zgzLtjWBJQizBtdAU0X8XoHFnW6cOK1Pa3O15Et812fs3HhWWTUj32GJnoQ==, tarball: file:projects/model-all.tgz}
resolution: {integrity: sha512-Q1uN1ojy8Xc1Q2hsrrBqQrwdhkJgfGu90yb/mtHmDDb+WCguVe1JlPL9jbLV0f39wY5qnCzByDApK2FHftgVlA==, tarball: file:projects/model-all.tgz}
id: file:projects/model-all.tgz
name: '@rush-temp/model-all'
version: 0.0.0
@ -12617,6 +12623,27 @@ packages:
- typescript
dev: false
file:projects/model-notification.tgz_typescript@4.5.4:
resolution: {integrity: sha512-q/JNneErW66xgCE5Yc/7bPkeUXwiEUJw95gbYt5uDTeuuzK0TrP77epbfSaWqkH7kP9SjqKrBwWPdm+M64q98g==, tarball: file:projects/model-notification.tgz}
id: file:projects/model-notification.tgz
name: '@rush-temp/model-notification'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.41.8
'@types/heft-jest': 1.0.2
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
'@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a
eslint-plugin-import: 2.25.3_eslint@7.32.0
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 5.2.0_eslint@7.32.0
prettier: 2.5.1
transitivePeerDependencies:
- supports-color
- typescript
dev: false
file:projects/model-presentation.tgz_typescript@4.5.4:
resolution: {integrity: sha512-cBkfqfxIgaViuDI4bj0NDuVg1NnzoSjL2mxvRRRn1Dnqkq+2K00cKyx67Pil9dHwH636h2HEYKoM4VNsT3Os1w==, tarball: file:projects/model-presentation.tgz}
id: file:projects/model-presentation.tgz
@ -12931,6 +12958,59 @@ packages:
- supports-color
dev: false
file:projects/notification-resources.tgz_ac194b5590200ebf8338e0f86ec190f4:
resolution: {integrity: sha512-TgP/VzfwZ+LGqaToY+rKYjH683GBuSHetD9bM85Jg6mtCtU09uogF3mwqKCAU1gyKtiVYLA63VmAb8MkxwV56A==, tarball: file:projects/notification-resources.tgz}
id: file:projects/notification-resources.tgz
name: '@rush-temp/notification-resources'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.41.8
'@types/heft-jest': 1.0.2
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
'@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a
eslint-plugin-import: 2.25.3_eslint@7.32.0
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 5.2.0_eslint@7.32.0
prettier: 2.5.1
svelte: 3.44.3
svelte-check: 2.3.0_4374c622c67ed7479ff0e44c29d09bce
typescript: 4.5.4
transitivePeerDependencies:
- '@babel/core'
- coffeescript
- less
- node-sass
- postcss
- postcss-load-config
- pug
- sass
- stylus
- sugarss
- supports-color
dev: false
file:projects/notification.tgz:
resolution: {integrity: sha512-VOGvKSMA5Utp3zuIYzvaHujYwS2ObDBtjrzKJzWDn8RtQo8zW8nOYjEOIDDIvXKGZlpl/5v62pDsEBiCQ/yNhw==, tarball: file:projects/notification.tgz}
name: '@rush-temp/notification'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.41.8
'@types/heft-jest': 1.0.2
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
'@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a
eslint-plugin-import: 2.25.3_eslint@7.32.0
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 5.2.0_eslint@7.32.0
prettier: 2.5.1
typescript: 4.5.4
transitivePeerDependencies:
- supports-color
dev: false
file:projects/panel.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-RxiNEW/PUGlF6/JrOvYWaUVfmC4kVnrd1tUQBpUM9tcRQebTwjPIUF1+V19xT63Rer6sRmz4SWo9v6/BtRAJlw==, tarball: file:projects/panel.tgz}
id: file:projects/panel.tgz
@ -13047,7 +13127,7 @@ packages:
dev: false
file:projects/presentation.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-QhfoHLyegkl3JsleXaZkSWsVQgikJM9jdqkla2mRW3kHyQAYWPBITUnVVWu1qBADViJupNZdYmYEcm7oeMF5og==, tarball: file:projects/presentation.tgz}
resolution: {integrity: sha512-jijMKCYTGrkmr9ShyFIPRnr3Uxyy8gyk8SDmwr9Wl4VevuSO9S/vHE0Pln+bEcKDtnzM/Vzh3GJmwUq3aPV3Aw==, tarball: file:projects/presentation.tgz}
id: file:projects/presentation.tgz
name: '@rush-temp/presentation'
version: 0.0.0
@ -13083,7 +13163,7 @@ packages:
dev: false
file:projects/prod.tgz_sass@1.45.0+typescript@4.5.4:
resolution: {integrity: sha512-mb0NOzQOQI/mZjzWLO+zp8F57x5koGPRF0qdrONOBHgKJ+wKpbSrVyW911S0nM8b/mVimVqaXCI2XdjAvYO1mg==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-8jdkx/UlSEQLfZMUQ/XwQoqOiGAZtR6yJehJqk8TrjkUWKD4y0kroErjmQZ1sv014LnaWuux3mjbte+9b79I7w==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
name: '@rush-temp/prod'
version: 0.0.0
@ -13157,7 +13237,7 @@ packages:
dev: false
file:projects/recruit-resources.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-p9e/9INuzm1qqr+zo6pCU0XRtFlQPObX3USd3gRr4hSt/YnFOD5+flKwk4LMCPtLrubdFhTAPX+Ydu1cHJrnnQ==, tarball: file:projects/recruit-resources.tgz}
resolution: {integrity: sha512-NgcfcFmKVD0ojJbtjT3yvD//m1x3koZpBdkLryhSKtQMhzcuZD9GpBmVbRnPtLGBrDMSALasx1NB5JMxxDPLFQ==, tarball: file:projects/recruit-resources.tgz}
id: file:projects/recruit-resources.tgz
name: '@rush-temp/recruit-resources'
version: 0.0.0
@ -13611,7 +13691,7 @@ packages:
dev: false
file:projects/telegram-resources.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-ZneaA8uvCmjuSf+zjIrPM1lzhi1At5Giud4UOzKwz2o0lrC4+npRTtCIZ52ZKjjNoOTmgf/hr9RYrqwCMH+z4g==, tarball: file:projects/telegram-resources.tgz}
resolution: {integrity: sha512-hreZkJ3tRwh2rXC7K/WlkA99TDatMLuHwtC2dTXqivRNEKhwH0RHDXgO1jrjulj7uSNj5+0ksOeQtlWBYmhRiw==, tarball: file:projects/telegram-resources.tgz}
id: file:projects/telegram-resources.tgz
name: '@rush-temp/telegram-resources'
version: 0.0.0

View File

@ -97,6 +97,8 @@
"@anticrm/templates": "~0.6.0",
"@anticrm/templates-assets": "~0.6.0",
"@anticrm/templates-resources": "~0.6.0",
"@anticrm/notification": "~0.6.0",
"@anticrm/notification-resources": "~0.6.0",
"@anticrm/core": "~0.6.16",
"@anticrm/rekoni": "~0.6.0"
}

View File

@ -33,6 +33,7 @@ import { gmailId } from '@anticrm/gmail'
import { imageCropperId } from '@anticrm/image-cropper'
import { inventoryId } from '@anticrm/inventory'
import { templatesId } from '@anticrm/templates'
import { notificationId } from '@anticrm/notification'
import rekoni from '@anticrm/rekoni'
import '@anticrm/login-assets'
@ -86,4 +87,6 @@ export async function configurePlatform() {
addLocation(imageCropperId, () => import(/* webpackChunkName: "image-cropper" */ '@anticrm/image-cropper-resources'))
addLocation(inventoryId, () => import(/* webpackChunkName: "inventory" */ '@anticrm/inventory-resources'))
addLocation(templatesId, () => import(/* webpackChunkName: "templates" */ '@anticrm/templates-resources'))
addLocation(templatesId, () => import(/* webpackChunkName: "templates" */ '@anticrm/templates-resources'))
addLocation(notificationId, () => import(/* webpackChunkName: "notification" */ '@anticrm/notification-resources'))
}

View File

@ -50,6 +50,7 @@
"@anticrm/model-inventory": "~0.6.0",
"@anticrm/model-presentation": "~0.6.0",
"@anticrm/model-templates": "~0.6.0",
"@anticrm/model-notification": "~0.6.0",
"@anticrm/model-text-editor": "~0.6.0",
"@anticrm/core": "~0.6.13"
}

View File

@ -36,6 +36,7 @@ import { createModel as templatesModel } from '@anticrm/model-templates'
import { createModel as textEditorModel } from '@anticrm/model-text-editor'
import { createModel as viewModel } from '@anticrm/model-view'
import { createModel as workbenchModel } from '@anticrm/model-workbench'
import { createModel as notificationModel } from '@anticrm/model-notification'
const builder = new Builder()
@ -57,7 +58,7 @@ const builders = [
presentationModel,
templatesModel,
textEditorModel,
notificationModel,
serverCoreModel,
serverAttachmentModel,
serverContactModel,

View File

@ -70,6 +70,8 @@ export class TChannel extends TAttachedDoc implements Channel {
@Prop(TypeString(), 'Value' as IntlString)
value!: string
items?: number
}
@Model(contact.class.Person, contact.class.Contact)

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@anticrm/model-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,18 @@
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
/**
* (Required) The name of the rig package to inherit from.
* It should be an NPM package name with the "-rig" suffix.
*/
"rigPackageName": "@anticrm/model-rig"
/**
* (Optional) Selects a config profile from the rig package. The name must consist of
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
* If omitted, then the "default" profile will be used."
*/
// "rigProfile": "your-profile-name"
}

View File

@ -0,0 +1,34 @@
{
"name": "@anticrm/model-notification",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"build:watch": "tsc",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src"
},
"devDependencies": {
"@anticrm/model-rig": "~0.6.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint": "^7.32.0",
"@types/heft-jest": "^1.0.2",
"@typescript-eslint/parser": "^5.4.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"prettier": "^2.4.1",
"@rushstack/heft": "^0.41.1"
},
"dependencies": {
"@anticrm/core": "~0.6.11",
"@anticrm/model": "~0.6.0",
"@anticrm/platform": "~0.6.5",
"@anticrm/model-core": "~0.6.0",
"@anticrm/notification": "~0.6.0"
}
}

View File

@ -0,0 +1,36 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022 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.
//
import type { Account, Domain, Ref, Timestamp } from '@anticrm/core'
import { Builder, Model, Prop, TypeRef, TypeTimestamp } from '@anticrm/model'
import core, { TAttachedDoc } from '@anticrm/model-core'
import notificaton, { LastView } from '@anticrm/notification'
import type { IntlString } from '@anticrm/platform'
export const DOMAIN_NOTIFICATION = 'notification' as Domain
@Model(notificaton.class.LastView, core.class.AttachedDoc, DOMAIN_NOTIFICATION)
export class TLastView extends TAttachedDoc implements LastView {
@Prop(TypeTimestamp(), 'Last View' as IntlString)
lastView!: Timestamp
@Prop(TypeRef(core.class.Account), 'Modified By' as IntlString)
user!: Ref<Account>
}
export function createModel (builder: Builder): void {
builder.createModel(TLastView)
}

View File

@ -0,0 +1,8 @@
{
"extends": "./node_modules/@anticrm/model-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
}
}

View File

@ -55,6 +55,7 @@ export default plugin(coreId, {
},
space: {
Tx: '' as Ref<Space>,
DerivedTx: '' as Ref<Space>,
Model: '' as Ref<Space>
},
account: {

View File

@ -30,7 +30,6 @@ export { default as AttributesBar } from './components/AttributesBar.svelte'
export { default as AttributeBarEditor } from './components/AttributeBarEditor.svelte'
export { default as AttributeEditor } from './components/AttributeEditor.svelte'
export { default as Card } from './components/Card.svelte'
export { default as Channels } from './components/Channels.svelte'
export { default as PDFViewer } from './components/PDFViewer.svelte'
export { default as MessageBox } from './components/MessageBox.svelte'
export { default as SpaceCreateCard } from './components/SpaceCreateCard.svelte'

View File

@ -22,11 +22,9 @@ import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform'
import { LiveQuery as LQ } from '@anticrm/query'
import { onDestroy } from 'svelte'
import contact, { ChannelProvider } from '@anticrm/contact'
let liveQuery: LQ
let client: TxOperations
let channelProviders: Promise<ChannelProvider[]> | undefined
class UIClient extends TxOperations implements Client {
constructor (client: Client, private readonly liveQuery: LQ) {
@ -49,7 +47,6 @@ export function setClient (_client: Client): void {
_client.notify = (tx: Tx) => {
liveQuery.tx(tx).catch((err) => console.log(err))
}
channelProviders = client.findAll(contact.class.ChannelProvider, {})
}
export class LiveQuery {
@ -113,12 +110,3 @@ export function getAttributePresenterClass (attribute: AnyAttribute): Ref<Class<
}
return attrClass
}
export async function getChannelProviders (): Promise<Map<Ref<ChannelProvider>, ChannelProvider>> {
const cp = (await channelProviders) ?? []
const map = new Map<Ref<ChannelProvider>, ChannelProvider>()
for (const provider of cp) {
map.set(provider._id, provider)
}
return map
}

View File

@ -116,6 +116,7 @@
bind:this={componentInstance}
_id={props._id}
_class={props._class}
rightSection={props.rightSection}
on:update={fitPopup}
on:close={_close}
/>

View File

@ -7,6 +7,7 @@ export interface PanelProps {
_id: string
_class: string
element?: PopupAlignment
rightSection?: AnyComponent
}
export const panelstore = writable < {panel?: PanelProps|undefined}>({ panel: undefined })
@ -23,7 +24,8 @@ export function showPanel (
component: AnyComponent,
_id: string,
_class: string,
element?: PopupAlignment
element?: PopupAlignment,
rightSection?: AnyComponent
): void {
const newLoc = encodeURIComponent([component, _id, _class].join('|'))
if (currentLocation === newLoc) {
@ -31,7 +33,7 @@ export function showPanel (
}
currentLocation = newLoc
panelstore.update(() => {
return { panel: { component, _id, _class, element } }
return { panel: { component, _id, _class, element, rightSection } }
})
const location = getCurrentLocation()
if (location.fragment !== currentLocation) {

View File

@ -39,6 +39,7 @@
"svelte": "^3.37.0",
"@anticrm/text-editor": "~0.6.0",
"@anticrm/contact": "~0.6.2",
"@anticrm/contact-resources": "~0.6.0",
"@anticrm/view-resources": "~0.6.0",
"@anticrm/view": "~0.6.0"
}

View File

@ -40,6 +40,7 @@
"@anticrm/core": "~0.6.11",
"@anticrm/view": "~0.6.0",
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/notification-resources": "~0.6.0",
"@anticrm/panel": "~0.6.0",
"@anticrm/view-resources": "~0.6.0",
"@anticrm/attachment": "~0.6.1"

View File

@ -16,9 +16,10 @@
<script lang="ts">
import { Channel } from '@anticrm/contact'
import type { AttachedData, Doc, Ref } from '@anticrm/core'
import presentation, { Channels } from '@anticrm/presentation'
import presentation from '@anticrm/presentation'
import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import ChannelsView from './ChannelsView.svelte'
import contact from '../plugin'
export let integrations: Set<Ref<Doc>> | undefined = undefined
@ -40,7 +41,7 @@
/>
<span><Label label={presentation.string.AddSocialLinks} /></span>
{:else}
<Channels value={channels} size={'small'} {integrations} on:click />
<ChannelsView value={channels} size={'small'} {integrations} on:click />
<div class="ml-1">
<CircleButton
icon={contact.icon.Edit}

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import type { AttachedData, Class, Doc, Ref } from '@anticrm/core'
import type { Class, Doc, Ref } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { ChannelProvider, Channel } from '@anticrm/contact'
@ -35,40 +35,72 @@
}
const query = createQuery()
query.query(contact.class.Channel, {
attachedTo: attachedTo
}, (res) => {
channels = res
})
query.query(
contact.class.Channel,
{
attachedTo: attachedTo
},
(res) => {
channels = res
}
)
const client = getClient()
async function save (newValues: AttachedData<Channel>[]): Promise<void> {
async function save (newValues: Channel[]): Promise<void> {
const currentProviders = new Set(channels.map((p) => p.provider))
const promises = []
for (const value of newValues) {
const oldChannel = findValue(value.provider)
if (oldChannel === undefined) {
if (oldChannel === undefined) {
if (value.value.length === 0) continue
promises.push(client.addCollection(contact.class.Channel, contact.space.Contacts, attachedTo, attachedClass, 'channels', {
value: value.value,
provider: value.provider
}))
promises.push(
client.addCollection(contact.class.Channel, contact.space.Contacts, attachedTo, attachedClass, 'channels', {
value: value.value,
provider: value.provider
})
)
} else {
currentProviders.delete(value.provider)
if (value.value === oldChannel.value) continue
promises.push(client.updateCollection(oldChannel._class, oldChannel.space, oldChannel._id, oldChannel.attachedTo, oldChannel.attachedToClass, oldChannel.collection, {
value: value.value
}))
promises.push(
client.updateCollection(
oldChannel._class,
oldChannel.space,
oldChannel._id,
oldChannel.attachedTo,
oldChannel.attachedToClass,
oldChannel.collection,
{
value: value.value
}
)
)
}
}
for (const value of currentProviders) {
const oldChannel = findValue(value)
if (oldChannel === undefined) continue
promises.push(client.removeCollection(oldChannel._class, oldChannel.space, oldChannel._id, oldChannel.attachedTo, oldChannel.attachedToClass, oldChannel.collection))
promises.push(
client.removeCollection(
oldChannel._class,
oldChannel.space,
oldChannel._id,
oldChannel.attachedTo,
oldChannel.attachedToClass,
oldChannel.collection
)
)
}
Promise.all(promises)
}
</script>
<Channels {channels} {integrations} on:change={(e) => { save(e.detail) }} on:click />
<Channels
{channels}
{integrations}
on:change={(e) => {
save(e.detail)
}}
on:click
/>

View File

@ -15,9 +15,26 @@
-->
<script lang="ts">
import type { Channel } from '@anticrm/contact'
import { Channels } from '@anticrm/presentation'
import ChannelsView from './ChannelsView.svelte'
import { showPanel } from '@anticrm/ui'
import view from '@anticrm/view'
export let value: Channel[] | Channel | null
function click (ev: any) {
if (ev.detail.presenter !== undefined && Array.isArray(value)) {
const channel = value[0]
if (channel !== undefined) {
showPanel(
view.component.EditDoc,
channel.attachedTo,
channel.attachedToClass,
'full',
ev.detail.presenter
)
}
}
}
</script>
<Channels {value} size={'small'} reverse />
<ChannelsView {value} size={'small'} reverse on:click={click} />

View File

@ -15,19 +15,21 @@
-->
<script lang="ts">
import type { Channel, ChannelProvider } from '@anticrm/contact'
import type { AttachedData, Doc, Ref } from '@anticrm/core'
import type { AttachedData, Doc, Ref, Timestamp } from '@anticrm/core'
import type { Asset, IntlString } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import { CircleButton, Tooltip } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { getClient } from '..'
import { getChannelProviders } from '../utils'
import ChannelsPopup from './ChannelsPopup.svelte'
import { NotificationClient } from '@anticrm/notification-resources'
export let value: AttachedData<Channel>[] | AttachedData<Channel> | null
export let value: AttachedData<Channel>[] | Channel | null
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'large'
export let reverse: boolean = false
export let integrations: Set<Ref<Doc>> = new Set<Ref<Doc>>()
const notificationClient = NotificationClient.getClient()
const lastViews = notificationClient.getLastViews()
interface Item {
label: IntlString
@ -35,19 +37,25 @@
value: string
presenter?: AnyComponent
integration: boolean
notification: boolean
}
const client = getClient()
const dispatch = createEventDispatcher()
function getProvider (item: AttachedData<Channel>, map: Map<Ref<ChannelProvider>, ChannelProvider>): any | undefined {
function getProvider (
item: AttachedData<Channel>,
map: Map<Ref<ChannelProvider>, ChannelProvider>,
lastViews: Map<Ref<Doc>, Timestamp>
): any | undefined {
const provider = map.get(item.provider)
if (provider) {
const notification = (item as Channel)._id !== undefined ? isNew((item as Channel), lastViews) : false
return {
label: provider.label as IntlString,
icon: provider.icon as Asset,
value: item.value,
presenter: provider.presenter,
notification,
integration: provider.integrationType !== undefined ? integrations.has(provider.integrationType) : false
}
} else {
@ -55,18 +63,27 @@
}
}
async function update (value: AttachedData<Channel>[] | AttachedData<Channel>) {
function isNew (item: Channel, lastViews: Map<Ref<Doc>, Timestamp>): boolean {
const lastView = (item as Channel)._id !== undefined ? lastViews.get((item as Channel)._id) : undefined
return lastView ? lastView < item.modifiedOn : (item.items ?? 0) > 0
}
async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: Map<Ref<Doc>, Timestamp>) {
if (value === null) {
displayItems = []
return
}
const result = []
const map = await getChannelProviders()
if (Array.isArray(value)) {
for (const item of value) {
const provider = getProvider(item, map)
const provider = getProvider(item, map, lastViews)
if (provider !== undefined) {
result.push(provider)
}
}
} else {
const provider = getProvider(value, map)
const provider = getProvider(value, map, lastViews)
if (provider !== undefined) {
result.push(provider)
}
@ -74,7 +91,7 @@
displayItems = result
}
$: if (value) update(value)
$: if (value) update(value, $lastViews)
let displayItems: Item[] = []
let divHTML: HTMLElement
@ -94,7 +111,7 @@
}}
>
<Tooltip component={ChannelsPopup} props={{ value: item }} label={undefined} anchor={divHTML}>
<CircleButton icon={item.icon} {size} primary={item.integration} />
<CircleButton icon={item.icon} {size} primary={item.integration || item.notification} />
</Tooltip>
</div>
{/each}

View File

@ -23,7 +23,7 @@
import { Channel, Organization } from '@anticrm/contact'
import contact from '../plugin'
import Company from './icons/Company.svelte'
import { generateId } from '@anticrm/core'
import { AttachedData, generateId } from '@anticrm/core'
import Channels from './Channels.svelte'
export function canClose (): boolean {
@ -51,7 +51,7 @@
dispatch('close')
}
let channels: Channel[] = []
let channels: AttachedData<Channel>[] = []
</script>
<Card

View File

@ -15,7 +15,7 @@
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Data, generateId } from '@anticrm/core'
import { AttachedData, Data, generateId } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { getClient, Card, EditableAvatar } from '@anticrm/presentation'
@ -72,7 +72,7 @@
dispatch('close')
}
let channels: Channel[] = []
let channels: AttachedData<Channel>[] = []
</script>
<Card

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import type { AttachedData, Ref } from '@anticrm/core'
import { AttachedData, Ref } from '@anticrm/core'
import { createEventDispatcher } from 'svelte'
import { EditBox, Button, ScrollBox } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
@ -42,14 +42,14 @@
for (const provider of providers) {
const i = findValue(provider._id)
if (i !== -1) {
newValues.push({ provider: provider._id, value: values[i].value })
newValues.push(values[i])
} else {
newValues.push({ provider: provider._id, value: '' })
}
}
})
function filterUndefined (channels: AttachedData<Channel>[]): AttachedData<Channel>[] {
function filterUndefined (channels: AttachedData<Channel>[]): AttachedData<Channel>[] {
return channels.filter((channel) => channel.value !== undefined && channel.value.length > 0)
}
</script>

View File

@ -21,6 +21,7 @@ import { Avatar, ObjectSearchResult, UserInfo } from '@anticrm/presentation'
import ChannelsEditor from './components/ChannelsEditor.svelte'
import Channels from './components/Channels.svelte'
import ChannelsPresenter from './components/ChannelsPresenter.svelte'
import ChannelsView from './components/ChannelsView.svelte'
import ContactPresenter from './components/ContactPresenter.svelte'
import Contacts from './components/Contacts.svelte'
import CreateOrganization from './components/CreateOrganization.svelte'
@ -34,7 +35,7 @@ import PersonPresenter from './components/PersonPresenter.svelte'
import SocialEditor from './components/SocialEditor.svelte'
import contact from './plugin'
export { Channels, ChannelsEditor, ContactPresenter }
export { Channels, ChannelsEditor, ContactPresenter, ChannelsView }
async function queryContact (_class: Ref<Class<Contact>>, client: Client, search: string): Promise<ObjectSearchResult[]> {
return (await client.findAll(_class, { name: { $like: `%${search}%` } }, { limit: 200 })).map(e => ({

View File

@ -0,0 +1,33 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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.
//
import contact, { ChannelProvider } from '@anticrm/contact'
import { Ref } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
let channelProviders: Promise<ChannelProvider[]> | undefined
const client = getClient()
channelProviders = client.findAll(contact.class.ChannelProvider, {})
export async function getChannelProviders (): Promise<Map<Ref<ChannelProvider>, ChannelProvider>> {
const cp = (await channelProviders) ?? []
const map = new Map<Ref<ChannelProvider>, ChannelProvider>()
for (const provider of cp) {
map.set(provider._id, provider)
}
return map
}

View File

@ -43,6 +43,7 @@ export interface ChannelProvider extends Doc, UXObject {
export interface Channel extends AttachedDoc {
provider: Ref<ChannelProvider>
value: string
items?: number
}
/**

View File

@ -41,6 +41,7 @@
"@anticrm/setting": "~0.6.0",
"@anticrm/chunter": "~0.6.0",
"@anticrm/chunter-resources": "~0.6.0",
"@anticrm/notification-resources": "~0.6.0",
"@anticrm/login": "~0.6.1",
"@anticrm/core": "~0.6.11"
}

View File

@ -13,7 +13,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createQuery, getClient } from '@anticrm/presentation'
import { Message, SharedMessage } from '@anticrm/gmail'
@ -25,12 +24,14 @@
import setting from '@anticrm/setting'
import Connect from './Connect.svelte'
import Messages from './Messages.svelte'
import { NotificationClient } from '@anticrm/notification-resources'
export let object: Contact
export let channel: Channel
export let newMessage: boolean
const EMAIL_REGEX = /(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
const EMAIL_REGEX =
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
let messages: Message[] = []
let accounts: EmployeeAccount[] = []
@ -42,22 +43,31 @@
const accauntsQuery = createQuery()
const settingsQuery = createQuery()
const accountId = getCurrentAccount()._id
const notificationClient = NotificationClient.getClient()
$: messagesQuery.query(
gmail.class.Message,
{ attachedTo: channel._id },
(res) => {
messages = res
},
{ sort: { modifiedOn: SortingOrder.Descending } }
)
function updateMessagesQuery (channelId: Ref<Channel>): void {
messagesQuery.query(
gmail.class.Message,
{ attachedTo: channelId },
(res) => {
messages = res
notificationClient.updateLastView(channelId, channel._class)
const accountsIds = new Set(messages.map((p) => p.modifiedBy as Ref<EmployeeAccount>))
updateAccountsQuery(accountsIds)
},
{ sort: { modifiedOn: SortingOrder.Descending } }
)
}
$: accountsIds = messages.map((p) => p.modifiedBy as Ref<EmployeeAccount>)
$: accauntsQuery.query(contact.class.EmployeeAccount, { _id: { $in: accountsIds }}, (result) => {
accounts = result
})
$: updateMessagesQuery(channel._id)
$: settingsQuery.query(
function updateAccountsQuery (accountsIds: Set<Ref<EmployeeAccount>>): void {
accauntsQuery.query(contact.class.EmployeeAccount, { _id: { $in: Array.from(accountsIds) } }, (result) => {
accounts = result
})
}
settingsQuery.query(
setting.class.Integration,
{ type: gmail.integrationType.Gmail, space: accountId as string as Ref<Space> },
(res) => {
@ -68,9 +78,17 @@
async function share (): Promise<void> {
const selectedMessages = messages.filter((m) => selected.has(m._id as string as Ref<SharedMessage>))
await client.addCollection(gmail.class.SharedMessages, object.space, object._id, object._class, 'gmailSharedMessages', {
messages: convertMessages(selectedMessages, accounts)
})
await client.addCollection(
gmail.class.SharedMessages,
object.space,
object._id,
object._class,
'gmailSharedMessages',
{
messages: convertMessages(selectedMessages, accounts)
}
)
await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
clear()
}
@ -91,7 +109,6 @@
})
}
function getName (message: Message, accounts: EmployeeAccount[], sender: boolean): string {
if (message.incoming === sender) {
return `${formatName(object.name)} (${channel.value})`

View File

@ -66,7 +66,7 @@
{#if currentMessage.copy?.length}
<Label label={gmail.string.Copy} />: {currentMessage.copy.join(', ')}
{/if}
<div class="flex-col clear-mins mt-9">
<div class="flex-col h-full clear-mins mt-9">
<FullMessageContent content={currentMessage.content} />
</div>
</div>

View File

@ -15,20 +15,18 @@
-->
<script lang="ts">
export let content: string
let editor: HTMLDivElement
$: if (editor) editor.innerHTML = content
</script>
<div bind:this={editor} class="input clear-mins" />
<iframe srcdoc={content} title="e-mail" />
<style lang="scss">
.input {
iframe {
overflow: auto;
padding: 1rem;
border: none;
border-radius: 0.5rem;
height: 100%;
background-color: #fff;
color: #1f212b;
border-radius: .5rem;
:global(a) {
font: inherit;

View File

@ -40,7 +40,7 @@
{#if message.copy?.length}
<Label label={gmail.string.Copy} />: {message.copy.join(', ')}
{/if}
<div class="flex-col clear-mins mt-5">
<div class="flex-col h-full clear-mins mt-5">
<FullMessageContent content={message.content} />
</div>
</div>
@ -51,7 +51,7 @@
max-height: 500px;
background-color: var(--theme-button-bg-focused);
border: 1px solid var(--theme-button-border-enabled);
border-radius: .75rem;
box-shadow: 0px 10px 20px rgba(0, 0, 0, .2);
border-radius: 0.75rem;
box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2);
}
</style>

View File

@ -20,18 +20,24 @@
import FullMessage from './FullMessage.svelte'
import Chats from './Chats.svelte'
import { getClient } from '@anticrm/presentation'
import { NotificationClient } from '@anticrm/notification-resources'
export let object: Contact
let newMessage: boolean = false
let currentMessage: SharedMessage | undefined = undefined
let channel: Channel | undefined = undefined
const notificationClient = NotificationClient.getClient()
const client = getClient()
client.findOne(contact.class.Channel, {
attachedTo: object._id,
provider: contact.channelProvider.Email
}).then((res) => channel = res)
client
.findOne(contact.class.Channel, {
attachedTo: object._id,
provider: contact.channelProvider.Email
})
.then((res) => {
channel = res
})
function back () {
if (newMessage) {
@ -40,8 +46,11 @@
return (currentMessage = undefined)
}
function selectHandler (e: CustomEvent) {
async function selectHandler (e: CustomEvent): Promise<void> {
currentMessage = e.detail
if (channel !== undefined) {
await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
}
}
</script>

View File

@ -15,9 +15,10 @@
-->
<script lang="ts">
import type { SharedMessage } from '@anticrm/gmail'
import { CheckBox } from '@anticrm/ui'
import { CheckBox, Label } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { getTime } from '../utils'
import gmail from '../plugin'
export let message: SharedMessage
export let selected: boolean = false
@ -37,10 +38,14 @@
{/if}
<div class="flex-col message" class:selected>
<div class="flex-between text-sm mb-1">
<div class="content-trans-color overflow-label mr-4">From: <span class="content-accent-color">{message.sender}</span></div>
<div class="content-trans-color overflow-label mr-4">
<Label label={gmail.string.From} /><span class="content-accent-color">{message.sender}</span>
</div>
<div class="content-trans-color">{getTime(message.modifiedOn)}</div>
</div>
<div class="content-trans-color text-sm overflow-label mr-4 mb-4">To: <span class="content-accent-color">{message.receiver}</span></div>
<div class="content-trans-color text-sm overflow-label mr-4 mb-4">
<Label label={gmail.string.To} /><span class="content-accent-color">{message.receiver}</span>
</div>
<div class="fs-title overflow-label mb-1">
{message.subject}
</div>
@ -60,10 +65,12 @@
padding: 1rem;
min-width: 0;
background-color: var(--theme-incoming-msg);
border-radius: .75rem;
border-radius: 0.75rem;
white-space: nowrap;
flex-grow: 1;
&.selected { background-color: var(--primary-button-enabled); }
&.selected {
background-color: var(--primary-button-enabled);
}
}
</style>

View File

@ -24,10 +24,12 @@
import { Channel, Contact, formatName } from '@anticrm/contact'
import { TextEditor } from '@anticrm/text-editor'
import plugin from '../plugin'
import { NotificationClient } from '@anticrm/notification-resources'
export let object: Contact
export let channel: Channel
export let currentMessage: SharedMessage | undefined
const notificationClient = NotificationClient.getClient()
let editor: TextEditor
let copy: string = ''
@ -42,7 +44,7 @@
const url = getMetadata(login.metadata.GmailUrl) ?? ''
async function sendMsg () {
fetch(url + '/send', {
await fetch(url + '/send', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getMetadata(login.metadata.LoginToken),
@ -53,6 +55,7 @@
copy: copy.split(',').map((m) => m.trim())
})
})
await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
dispatch('close')
}
@ -130,7 +133,7 @@
background-color: #fff;
color: #1f212b;
height: 100%;
border-radius: .5rem;
border-radius: 0.5rem;
:global(.ProseMirror) {
min-height: 0;

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,18 @@
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
/**
* (Required) The name of the rig package to inherit from.
* It should be an NPM package name with the "-rig" suffix.
*/
"rigPackageName": "@anticrm/platform-rig"
/**
* (Optional) Selects a config profile from the rig package. The name must consist of
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
* If omitted, then the "default" profile will be used."
*/
// "rigProfile": "your-profile-name"
}

View File

@ -0,0 +1,37 @@
{
"name": "@anticrm/notification-resources",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"build:watch": "tsc",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src",
"svelte-check": "svelte-check"
},
"devDependencies": {
"@anticrm/platform-rig": "~0.6.0",
"@types/heft-jest": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint": "^7.32.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"prettier": "^2.4.1",
"@rushstack/heft": "^0.41.1",
"typescript": "^4.3.5",
"svelte-check": "~2.3.0"
},
"dependencies": {
"@anticrm/presentation": "~0.6.2",
"@anticrm/platform": "~0.6.5",
"@anticrm/core": "~0.6.11",
"@anticrm/notification": "~0.6.0",
"svelte": "^3.37.0"
}
}

View File

@ -0,0 +1,21 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022 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.
//
import { Resources } from '@anticrm/platform'
export default async (): Promise<Resources> => ({})
export * from './utils'

View File

@ -0,0 +1,89 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022 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.
//
import { Class, Doc, getCurrentAccount, Ref, Timestamp } from '@anticrm/core'
import notification, { LastView } from '@anticrm/notification'
import { createQuery, getClient } from '@anticrm/presentation'
import { writable, Writable } from 'svelte/store'
/**
* @public
*/
export class NotificationClient {
protected static _instance: NotificationClient | undefined = undefined
private readonly lastViews = new Map<Ref<Doc>, LastView>()
private readonly lastViewsStore = writable(new Map<Ref<Doc>, Timestamp>())
private readonly lastViewQuery = createQuery()
private constructor () {
this.lastViewQuery.query(notification.class.LastView, { user: getCurrentAccount()._id }, (result) => {
const res: Map<Ref<Doc>, Timestamp> = new Map<Ref<Doc>, Timestamp>()
result.forEach((p) => {
res.set(p.attachedTo, p.lastView)
this.lastViews.set(p.attachedTo, p)
})
this.lastViewsStore.set(res)
})
}
static getClient (): NotificationClient {
if (NotificationClient._instance === undefined) {
NotificationClient._instance = new NotificationClient()
}
return NotificationClient._instance
}
getLastViews (): Writable<Map<Ref<Doc>, Timestamp>> {
return this.lastViewsStore
}
async updateLastView (
_id: Ref<Doc>,
_class: Ref<Class<Doc>>,
time?: Timestamp,
force: boolean = false
): Promise<void> {
const client = getClient()
const user = getCurrentAccount()._id
const lastView = time ?? new Date().getTime()
const current = this.lastViews.get(_id)
if (current !== undefined) {
if (current.lastView < lastView || force) {
await client.updateDoc(current._class, current.space, current._id, {
lastView: lastView
})
}
} else if (force) {
await client.createDoc(notification.class.LastView, notification.space.Notifications, {
user,
lastView,
attachedTo: _id,
attachedToClass: _class,
collection: 'lastViews'
})
}
}
async unsubscribe (_id: Ref<Doc>): Promise<void> {
const client = getClient()
const user = getCurrentAccount()._id
const current = await client.findOne(notification.class.LastView, { attachedTo: _id, user })
if (current !== undefined) {
await client.removeDoc(current._class, current.space, current._id)
}
}
}

View File

@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};

View File

@ -0,0 +1,8 @@
{
"extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,18 @@
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
/**
* (Required) The name of the rig package to inherit from.
* It should be an NPM package name with the "-rig" suffix.
*/
"rigPackageName": "@anticrm/platform-rig"
/**
* (Optional) Selects a config profile from the rig package. The name must consist of
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
* If omitted, then the "default" profile will be used."
*/
// "rigProfile": "your-profile-name"
}

View File

@ -0,0 +1,32 @@
{
"name": "@anticrm/notification",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"build:watch": "tsc",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src"
},
"devDependencies": {
"@anticrm/platform-rig": "~0.6.0",
"@types/heft-jest": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint": "^7.32.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"prettier": "^2.4.1",
"@rushstack/heft": "^0.41.1",
"typescript": "^4.3.5"
},
"dependencies": {
"@anticrm/platform": "~0.6.5",
"@anticrm/core": "~0.6.11"
}
}

View File

@ -0,0 +1,46 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022 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.
//
import type { Account, AttachedDoc, Class, Ref, Space, Timestamp } from '@anticrm/core'
import type { Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
/**
* @public
*/
export interface LastView extends AttachedDoc {
lastView: Timestamp
user: Ref<Account>
}
/**
* @public
*/
export const notificationId = 'notification' as Plugin
/**
* @public
*/
const notification = plugin(notificationId, {
class: {
LastView: '' as Ref<Class<LastView>>
},
space: {
Notifications: '' as Ref<Space>
}
})
export default notification

View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"lib": ["esnext", "dom"]
}
}

View File

@ -14,12 +14,24 @@
-->
<script lang="ts">
import { Avatar } from '@anticrm/presentation'
import { Avatar, createQuery } from '@anticrm/presentation'
import type { Candidate } from '@anticrm/recruit'
import { Channels } from '@anticrm/presentation'
import { formatName } from '@anticrm/contact'
import { ChannelsView } from '@anticrm/contact-resources'
import contact, { Channel, formatName } from '@anticrm/contact'
export let candidate: Candidate
let channels: Channel[] = []
const channelsQuery = createQuery()
channelsQuery.query(
contact.class.Channel,
{
attachedTo: candidate._id
},
(res) => {
channels = res
}
)
</script>
<div class="flex-col h-full card-container">
@ -29,7 +41,7 @@
<div class="name">{formatName(candidate.name)}</div>
<div class="description">{candidate.title ?? ''}</div>
<div class="description">{candidate.city ?? ''}</div>
<div class="footer"><Channels value={candidate.channels} size={'small'} /></div>
<div class="footer"><ChannelsView value={channels} size={'small'} on:click /></div>
{/if}
</div>

View File

@ -14,16 +14,15 @@
-->
<script lang="ts">
import attachment from '@anticrm/attachment'
import contact, { Channel, ChannelProvider, combineName, Person } from '@anticrm/contact'
import contact,{ Channel,ChannelProvider,combineName,Person } from '@anticrm/contact'
import { Channels } from '@anticrm/contact-resources'
import type { AttachedData, Data, MixinData, Ref } from '@anticrm/core'
import { generateId } from '@anticrm/core'
import { AttachedData, Data,generateId,MixinData,Ref } from '@anticrm/core'
import login from '@anticrm/login'
import { getMetadata, getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
import { Card, EditableAvatar, getClient, getFileUrl, PDFViewer } from '@anticrm/presentation'
import { getMetadata,getResource,setPlatformStatus,unknownError } from '@anticrm/platform'
import { Card,EditableAvatar,getClient,getFileUrl,PDFViewer } from '@anticrm/presentation'
import type { Candidate } from '@anticrm/recruit'
import { recognizeDocument } from '@anticrm/rekoni'
import { EditBox, IconFile as FileIcon, Label, Link, showPopup, Spinner } from '@anticrm/ui'
import { EditBox,IconFile as FileIcon,Label,Link,showPopup,Spinner } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
import FileUpload from './icons/FileUpload.svelte'

View File

@ -51,7 +51,7 @@
{#if object !== undefined && candidate !== undefined}
<div class="flex-between">
<div class="card"><CandidateCard {candidate} /></div>
<div class="card"><CandidateCard {candidate} on:click /></div>
<div class="arrows"><ExpandRightDouble /></div>
<div class="card"><VacancyCard {vacancy} /></div>
</div>

View File

@ -40,6 +40,7 @@
"@anticrm/setting": "~0.6.0",
"@anticrm/chunter": "~0.6.0",
"@anticrm/login": "~0.6.1",
"@anticrm/core": "~0.6.11"
"@anticrm/core": "~0.6.11",
"@anticrm/notification-resources": "~0.6.0"
}
}

View File

@ -14,28 +14,34 @@
// limitations under the License.
-->
<script lang="ts">
import contact,{ Channel,Contact,EmployeeAccount,formatName } from '@anticrm/contact'
import { getCurrentAccount,Ref,SortingOrder,Space } from '@anticrm/core'
import contact, { Channel, Contact, EmployeeAccount, formatName } from '@anticrm/contact'
import { getCurrentAccount, Ref, SortingOrder, Space } from '@anticrm/core'
import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform'
import { createQuery,getClient } from '@anticrm/presentation'
import { createQuery, getClient } from '@anticrm/presentation'
import setting from '@anticrm/setting'
import type { SharedTelegramMessage, TelegramMessage } from '@anticrm/telegram'
import { ReferenceInput } from '@anticrm/text-editor'
import { ActionIcon,Button,IconShare,ScrollBox,showPopup } from '@anticrm/ui'
import { ActionIcon, Button, IconShare, ScrollBox, showPopup } from '@anticrm/ui'
import telegram from '../plugin'
import Connect from './Connect.svelte'
import TelegramIcon from './icons/Telegram.svelte'
import Messages from './Messages.svelte'
import { NotificationClient } from '@anticrm/notification-resources'
export let object: Contact
let channel: Channel | undefined = undefined
const client = getClient()
const notificationClient = NotificationClient.getClient()
client.findOne(contact.class.Channel, {
attachedTo: object._id,
provider: contact.channelProvider.Telegram
}).then((res) => channel = res)
client
.findOne(contact.class.Channel, {
attachedTo: object._id,
provider: contact.channelProvider.Telegram
})
.then((res) => {
channel = res
})
let messages: TelegramMessage[] = []
let accounts: EmployeeAccount[] = []
@ -49,16 +55,31 @@
const settingsQuery = createQuery()
const accountId = getCurrentAccount()._id
$: channel && messagesQuery.query(telegram.class.Message, { modifiedBy: accountId, attachedTo: channel._id }, (res) => {
messages = res
}, { sort: { modifiedOn: SortingOrder.Ascending }})
function updateMessagesQuery (channelId: Ref<Channel>): void {
messagesQuery.query(
telegram.class.Message,
{ attachedTo: channelId },
(res) => {
messages = res.reverse()
if (channel !== undefined) {
notificationClient.updateLastView(channel._id, channel._class)
}
const accountsIds = new Set(messages.map((p) => p.modifiedBy as Ref<EmployeeAccount>))
updateAccountsQuery(accountsIds)
},
{ sort: { modifiedOn: SortingOrder.Descending }, limit: 500 }
)
}
$: accountsIds = messages.map((p) => p.modifiedBy as Ref<EmployeeAccount>)
$: accauntsQuery.query(contact.class.EmployeeAccount, { _id: { $in: accountsIds }}, (result) => {
accounts = result
})
$: channel && updateMessagesQuery(channel._id)
$: settingsQuery.query(
function updateAccountsQuery (accountsIds: Set<Ref<EmployeeAccount>>): void {
accauntsQuery.query(contact.class.EmployeeAccount, { _id: { $in: Array.from(accountsIds) } }, (result) => {
accounts = result
})
}
settingsQuery.query(
setting.class.Integration,
{ type: telegram.integrationType.Telegram, space: accountId as string as Ref<Space> },
(res) => {
@ -67,7 +88,7 @@
)
async function sendMsg (to: string, msg: string) {
return await fetch(url + '/send-msg', {
const res = await fetch(url + '/send-msg', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + getMetadata(login.metadata.LoginToken),
@ -78,6 +99,10 @@
msg
})
})
if (channel !== undefined) {
await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
}
return res
}
async function addContact (phone: string) {
@ -137,6 +162,9 @@
messages: convertMessages(selectedMessages, accounts)
}
)
if (channel !== undefined) {
await notificationClient.updateLastView(channel._id, channel._class, channel.modifiedOn, true)
}
clear()
}
@ -199,7 +227,13 @@
<Button label={telegram.string.Cancel} size={'small'} on:click={clear} />
</div>
<div class="ml-3">
<Button label={telegram.string.PublishSelected} size={'small'} primary disabled={!selected.size} on:click={share} />
<Button
label={telegram.string.PublishSelected}
size={'small'}
primary
disabled={!selected.size}
on:click={share}
/>
</div>
</div>
</div>

View File

@ -16,10 +16,23 @@
import type { SharedTelegramMessage } from '@anticrm/telegram'
export let message: SharedTelegramMessage
const current = new Date()
const target = new Date(message.modifiedOn)
let options: Intl.DateTimeFormatOptions = {
day: 'numeric',
month: 'long'
}
if (current.getFullYear() !== target.getFullYear()) {
options = {
...options,
year: '2-digit'
}
}
</script>
<div class="datetime-container">
{new Intl.DateTimeFormat('default', { day: 'numeric', month: 'long' }).format(message.modifiedOn)}
{new Intl.DateTimeFormat('default', options).format(message.modifiedOn)}
</div>
<style lang="scss">

View File

@ -32,10 +32,10 @@
export let _id: Ref<Doc>
export let _class: Ref<Class<Doc>>
export let rightSection: AnyComponent | undefined = undefined
let object: Doc
let objectClass: Class<Doc>
let parentClass: Ref<Class<Doc>>
let rightSection: AnyComponent | undefined
let fullSize: boolean = true
const client = getClient()

View File

@ -30,7 +30,7 @@ import core, {
} from '@anticrm/core'
import type { IntlString } from '@anticrm/platform'
import { getResource } from '@anticrm/platform'
import { Channels, getAttributePresenterClass } from '@anticrm/presentation'
import { getAttributePresenterClass } from '@anticrm/presentation'
import { ErrorPresenter, getPlatformColorForText } from '@anticrm/ui'
import type { Action, ActionTarget, BuildModelOptions, ObjectDDParticipant } from '@anticrm/view'
import view, { AttributeModel, BuildModelKey } from '@anticrm/view'

View File

@ -14,7 +14,7 @@
-->
<script lang="ts">
import contact, { Employee, EmployeeAccount } from '@anticrm/contact'
import core, { Client, getCurrentAccount, Ref, Space } from '@anticrm/core'
import core, { Client, Doc, getCurrentAccount, Ref, Space } from '@anticrm/core'
import { Avatar, createQuery, setClient } from '@anticrm/presentation'
import {
AnyComponent,
@ -235,7 +235,7 @@
</div>
<!-- <div class="aside"><Chat thread/></div> -->
</div>
<PanelInstance/>
<PanelInstance />
<Popup />
<TooltipInstance />
{:else}

View File

@ -985,6 +985,21 @@
"packageName": "@anticrm/rekoni",
"projectFolder": "packages/rekoni",
"shouldPublish": true
},
},
{
"packageName": "@anticrm/model-notification",
"projectFolder": "models/notification",
"shouldPublish": true
},
{
"packageName": "@anticrm/notification",
"projectFolder": "plugins/notification",
"shouldPublish": true
},
{
"packageName": "@anticrm/notification-resources",
"projectFolder": "plugins/notification-resources",
"shouldPublish": true
}
]
}

View File

@ -199,7 +199,9 @@ class TServerStorage implements ServerStorage {
const _class = txClass(tx)
const objClass = txObjectClass(tx)
return await ctx.with('tx', { _class, objClass }, async (ctx) => {
await ctx.with('domain-tx', { _class, objClass }, async () => await this.getAdapter(DOMAIN_TX).tx(tx))
if (tx.objectSpace !== core.space.DerivedTx) {
await ctx.with('domain-tx', { _class, objClass }, async () => await this.getAdapter(DOMAIN_TX).tx(tx))
}
if (tx.objectSpace === core.space.Model) {
// maintain hiearachy and triggers