Fix telegram/gmail notifications (#4976)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2024-03-15 08:03:50 +04:00 committed by GitHub
parent b7775cb26c
commit ecf2a5193a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 382 additions and 34 deletions

View File

@ -292,6 +292,18 @@ export function createModel (builder: Builder): void {
provider: contact.function.PersonTooltipProvider
})
builder.mixin(contact.class.Channel, core.class.Class, view.mixin.ObjectIdentifier, {
provider: contact.function.ChannelIdentifierProvider
})
builder.mixin(contact.class.Channel, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: contact.function.ChannelTitleProvider
})
builder.mixin(contact.class.Channel, core.class.Class, view.mixin.ObjectIcon, {
component: contact.component.ChannelIcon
})
builder.createDoc(
workbench.class.Application,
core.space.Model,

View File

@ -60,7 +60,8 @@ export default mergeIds(contactId, contact, {
ActivityChannelPresenter: '' as AnyComponent,
EmployeeFilter: '' as AnyComponent,
EmployeeFilterValuePresenter: '' as AnyComponent,
PersonAccountFilterValuePresenter: '' as AnyComponent
PersonAccountFilterValuePresenter: '' as AnyComponent,
ChannelIcon: '' as AnyComponent
},
string: {
Persons: '' as IntlString,
@ -137,6 +138,8 @@ export default mergeIds(contactId, contact, {
GetContactName: '' as Resource<TemplateFieldFunc>,
GetContactFirstName: '' as Resource<TemplateFieldFunc>,
GetContactLastName: '' as Resource<TemplateFieldFunc>,
ContactTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
ContactTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
ChannelTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
ChannelIdentifierProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
}
})

View File

@ -176,6 +176,19 @@ export function createModel (builder: Builder): void {
gmail.ids.TxSharedCreate
)
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: gmail.class.Message,
icon: contact.icon.Email,
action: 'create',
component: gmail.activity.GmailWriteMessage,
label: gmail.string.HaveWrittenEmail
},
gmail.ids.GmailWriteMessageActivityViewlet
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
@ -192,6 +205,20 @@ export function createModel (builder: Builder): void {
gmail.ids.TxSharedCreate
)
builder.createDoc(
activity.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: gmail.class.SharedMessages,
icon: contact.icon.Email,
action: 'create',
component: gmail.activity.GmailSharedMessage,
label: gmail.string.SharedMessages,
hideIfRemoved: true
},
gmail.ids.GmailSharedMessageActivityViewlet
)
createAction(
builder,
{

View File

@ -19,7 +19,7 @@ import { type IntlString, mergeIds, type Resource } from '@hcengineering/platfor
import { gmailId } from '@hcengineering/gmail'
import gmail from '@hcengineering/gmail-resources/src/plugin'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import type { TxViewlet } from '@hcengineering/activity'
import type { DocUpdateMessageViewlet, TxViewlet } from '@hcengineering/activity'
import { type Action } from '@hcengineering/view'
import { type NotificationGroup } from '@hcengineering/notification'
@ -45,11 +45,15 @@ export default mergeIds(gmailId, gmail, {
ids: {
TxSharedCreate: '' as Ref<TxViewlet>,
NewMessageNotification: '' as Ref<TxViewlet>,
EmailNotificationGroup: '' as Ref<NotificationGroup>
EmailNotificationGroup: '' as Ref<NotificationGroup>,
GmailSharedMessageActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
GmailWriteMessageActivityViewlet: '' as Ref<DocUpdateMessageViewlet>
},
activity: {
TxSharedCreate: '' as AnyComponent,
TxWriteMessage: '' as AnyComponent
TxWriteMessage: '' as AnyComponent,
GmailSharedMessage: '' as AnyComponent,
GmailWriteMessage: '' as AnyComponent
},
function: {
HasEmail: '' as Resource<(doc?: Doc | Doc[] | undefined) => Promise<boolean>>

View File

@ -144,7 +144,7 @@ export function createModel (builder: Builder): void {
action: 'create',
icon: contact.icon.Telegram,
component: telegram.activity.TelegramMessageCreated,
label: telegram.string.SharedMessages
label: telegram.string.SharedMessage
},
telegram.ids.TelegramMessageCreatedActivityViewlet
)

View File

@ -34,7 +34,7 @@ import {
getDocLinkTitle,
hasAttributePresenter
} from '@hcengineering/view-resources'
import { type Person } from '@hcengineering/contact'
import contact, { type Person } from '@hcengineering/contact'
import { type IntlString } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui'
import { get } from 'svelte/store'
@ -220,20 +220,42 @@ function combineByCreateThreshold (docUpdateMessages: DocUpdateMessage[]): DocUp
})
}
function wrapMessages (
hierarchy: Hierarchy,
messages: ActivityMessage[]
): { toCombine: DocUpdateMessage[], uncombined: ActivityMessage[] } {
const toCombine: DocUpdateMessage[] = []
const uncombined: ActivityMessage[] = []
for (const message of messages) {
if (isDocUpdateMessage(message)) {
if (hierarchy.isDerived(message.attachedToClass, contact.class.Channel)) {
uncombined.push(message)
} else {
toCombine.push(message)
}
} else {
uncombined.push(message)
}
}
return { toCombine, uncombined }
}
export async function combineActivityMessages (
messages: ActivityMessage[],
sortingOrder: SortingOrder = SortingOrder.Ascending
): Promise<DisplayActivityMessage[]> {
const client = getClient()
const uncombinedMessages = messages.filter((message) => message._class !== activity.class.DocUpdateMessage)
const docUpdateMessages = combineByCreateThreshold(messages.filter(isDocUpdateMessage))
const { uncombined, toCombine } = wrapMessages(client.getHierarchy(), messages)
const docUpdateMessages = combineByCreateThreshold(toCombine)
if (docUpdateMessages.length === 0) {
return sortActivityMessages(uncombinedMessages, sortingOrder)
return sortActivityMessages(uncombined, sortingOrder)
}
const result: Array<DisplayActivityMessage | undefined> = [...uncombinedMessages]
const result: Array<DisplayActivityMessage | undefined> = [...uncombined]
const groupedByType: Map<string, DocUpdateMessage[]> = groupByArray(docUpdateMessages, getDocUpdateMessageKey)

View File

@ -185,11 +185,14 @@
<ShowMore>
<div class="customContent">
{#each value?.previousMessages ?? [] as msg}
<Component is={viewlet.component} props={{ message: msg, _id: msg.objectId, _class: msg.objectClass }} />
<Component
is={viewlet.component}
props={{ message: msg, _id: msg.objectId, _class: msg.objectClass, onClick }}
/>
{/each}
<Component
is={viewlet.component}
props={{ message: value, _id: value.objectId, _class: value.objectClass, value: object }}
props={{ message: value, _id: value.objectId, _class: value.objectClass, value: object, onClick }}
/>
</div>
</ShowMore>

View File

@ -22,6 +22,7 @@ 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 contact from '@hcengineering/contact'
import { type ChatNavGroupModel, type ChatNavItemModel } from './types'
import chunter from '../../plugin'
@ -115,7 +116,8 @@ export const chatNavGroupModels: ChatNavGroupModel[] = [
query: {
isPinned: { $ne: true },
attachedToClass: {
$nin: [chunter.class.DirectMessage, chunter.class.Channel]
// Ignore external channels until support is provided for them
$nin: [chunter.class.DirectMessage, chunter.class.Channel, contact.class.Channel]
}
}
}

View File

@ -0,0 +1,40 @@
<!--
// 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 { Channel, ChannelProvider } from '@hcengineering/contact'
import { Icon, IconSize } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import { classIcon } from '@hcengineering/view-resources'
import contact from '../plugin'
export let value: Channel | undefined
export let size: IconSize = 'small'
const client = getClient()
let provider: ChannelProvider | undefined = undefined
$: value &&
client.findOne(contact.class.ChannelProvider, { _id: value.provider }).then((res) => {
provider = res
})
$: icon = provider?.icon ?? classIcon(client, contact.class.Channel)
</script>
{#if icon}
<Icon {size} {icon} />
{/if}

View File

@ -18,10 +18,12 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import { AnyComponent, Component } from '@hcengineering/ui'
import { channelProviders } from '../utils'
import { DocUpdateMessage } from '@hcengineering/activity'
export let _id: Ref<Channel>
export let _class: Ref<Class<Channel>>
export let embedded: boolean = false
export let activityMessage: DocUpdateMessage | undefined = undefined
const client = getClient()
@ -45,7 +47,14 @@
{#if presenter}
<Component
is={presenter}
props={{ embedded, _id: channel?.attachedTo, _class: channel?.attachedToClass, channel }}
props={{
embedded,
_id: channel?.attachedTo,
_class: channel?.attachedToClass,
channel,
messageId: activityMessage?.objectId
}}
on:close
/>
{/if}
{/await}

View File

@ -104,9 +104,12 @@ import SelectUsersPopup from './components/SelectUsersPopup.svelte'
import IconAddMember from './components/icons/AddMember.svelte'
import UserDetails from './components/UserDetails.svelte'
import EditOrganizationPanel from './components/EditOrganizationPanel.svelte'
import ChannelIcon from './components/ChannelIcon.svelte'
import contact from './plugin'
import {
channelIdentifierProvider,
channelTitleProvider,
contactTitleProvider,
employeeSort,
filterChannelHasMessagesResult,
@ -334,7 +337,8 @@ export default async (): Promise<Resources> => ({
DeleteConfirmationPopup,
PersonAccountRefPresenter,
PersonIcon,
EditOrganizationPanel
EditOrganizationPanel,
ChannelIcon
},
completion: {
EmployeeQuery: async (
@ -377,7 +381,9 @@ export default async (): Promise<Resources> => ({
GetContactLastName: getContactLastName,
GetContactLink: getContactLink,
ContactTitleProvider: contactTitleProvider,
PersonTooltipProvider: getPersonTooltip
PersonTooltipProvider: getPersonTooltip,
ChannelTitleProvider: channelTitleProvider,
ChannelIdentifierProvider: channelIdentifierProvider
},
resolver: {
Location: resolveLocation

View File

@ -26,7 +26,8 @@ import {
formatName,
getFirstName,
getLastName,
getName
getName,
type Channel
} from '@hcengineering/contact'
import {
type Client,
@ -41,7 +42,7 @@ import {
type Class
} from '@hcengineering/core'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { getEmbeddedLabel, getResource } from '@hcengineering/platform'
import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { type TemplateDataProvider } from '@hcengineering/templates'
import {
@ -402,3 +403,22 @@ export function getPersonTooltip (client: Client, value: Person | null | undefin
label: getEmbeddedLabel(getName(hierarchy, value))
}
}
export async function channelIdentifierProvider (client: Client, ref: Ref<Channel>, doc?: Channel): Promise<string> {
const channel = doc ?? (await client.findOne(contact.class.Channel, { _id: ref }))
if (channel === undefined) return ''
const provider = await client.findOne(contact.class.ChannelProvider, { _id: channel.provider })
if (provider === undefined) return channel.value
return await translate(provider.label, {})
}
export async function channelTitleProvider (client: Client, ref: Ref<Channel>, doc?: Channel): Promise<string> {
const channel = doc ?? (await client.findOne(contact.class.Channel, { _id: ref }))
if (channel === undefined) return ''
return channel.value
}

View File

@ -16,7 +16,7 @@
<script lang="ts">
import contact, { Channel, Contact, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { getCurrentAccount } from '@hcengineering/core'
import { getCurrentAccount, Ref } from '@hcengineering/core'
import { Message, SharedMessage } from '@hcengineering/gmail'
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
@ -33,28 +33,41 @@
import IntegrationSelector from './IntegrationSelector.svelte'
import NewMessage from './NewMessage.svelte'
export let channel: Channel
export let channel: Channel | undefined
// export let embedded = false
export let message: Message | undefined = undefined
export let messageId: Ref<Message> | undefined = undefined
const client = getClient()
const inboxClient = InboxNotificationsClientImpl.getClient()
const messageQuery = createQuery()
let object: Contact
let gmailMessage: Message | undefined = message
let currentMessage: SharedMessage | undefined = undefined
let newMessage: boolean = false
let integrations: Integration[] = []
let selectedIntegration: Integration | undefined = undefined
inboxClient.forceReadDoc(getClient(), channel._id, channel._class)
channel && inboxClient.forceReadDoc(getClient(), channel._id, channel._class)
const dispatch = createEventDispatcher()
const query = createQuery()
$: query.query(channel.attachedToClass, { _id: channel.attachedTo }, (result) => {
object = result[0] as Contact
})
$: channel &&
query.query(channel.attachedToClass, { _id: channel.attachedTo }, (result) => {
object = result[0] as Contact
})
$: if (message === undefined && messageId !== undefined) {
messageQuery.query(gmail.class.Message, { _id: messageId }, (result) => {
gmailMessage = result[0] as Message
})
} else {
gmailMessage = message
}
function back () {
if (newMessage) {
@ -87,10 +100,10 @@
selectedIntegration = integrations.find((p) => p.createdBy === me) ?? integrations[0]
})
$: message &&
$: gmailMessage &&
channel &&
object &&
convertMessage(object, channel, message, $employeeByIdStore).then((p) => (currentMessage = p))
convertMessage(object, channel, gmailMessage, $employeeByIdStore).then((p) => (currentMessage = p))
</script>
{#if channel && object}

View File

@ -0,0 +1,46 @@
<!--
// 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 { createQuery } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import { SharedMessages } from '@hcengineering/gmail'
import gmail from '../../plugin'
import SharedMessagesView from '../SharedMessages.svelte'
export let _id: Ref<SharedMessages> | undefined = undefined
export let value: SharedMessages | undefined = undefined
const query = createQuery()
let doc: SharedMessages | undefined = undefined
$: loadObject(_id, value)
function loadObject (_id?: Ref<SharedMessages>, value?: SharedMessages): void {
if (value === undefined && _id !== undefined) {
query.query(gmail.class.SharedMessages, { _id }, (res) => {
doc = res[0]
})
} else {
doc = value
query.unsubscribe()
}
}
</script>
{#if doc}
<SharedMessagesView value={doc} />
{/if}

View File

@ -0,0 +1,69 @@
<!--
// 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 { createQuery, getClient } from '@hcengineering/presentation'
import { Ref } from '@hcengineering/core'
import { Message } from '@hcengineering/gmail'
import { showPopup } from '@hcengineering/ui'
import gmail from '../../plugin'
import Main from '../Main.svelte'
export let _id: Ref<Message> | undefined = undefined
export let value: Message | undefined = undefined
export let onClick: ((ev: MouseEvent) => void) | undefined = undefined
const query = createQuery()
let doc: Message | undefined = undefined
$: loadObject(_id, value)
function loadObject (_id?: Ref<Message>, value?: Message): void {
if (value === undefined && _id !== undefined) {
query.query(gmail.class.Message, { _id }, (res) => {
doc = res[0]
})
} else {
doc = value
query.unsubscribe()
}
}
async function click (ev: MouseEvent): Promise<void> {
ev.stopPropagation()
if (onClick) {
onClick(ev)
return
}
if (doc === undefined) {
return
}
const client = getClient()
const channel = await client.findOne(doc.attachedToClass, { _id: doc.attachedTo })
if (channel !== undefined) {
showPopup(Main, { channel, message: doc }, 'float')
}
}
</script>
{#if doc}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="over-underline overflow-label" on:click={click}>{doc.subject}</span>
{/if}

View File

@ -19,6 +19,8 @@ import { getMetadata, type Resources } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import TxSharedCreate from './components/activity/TxSharedCreate.svelte'
import TxWriteMessage from './components/activity/TxWriteMessage.svelte'
import GmailWriteMessage from './components/activity/GmailWriteMessage.svelte'
import GmailSharedMessage from './components/activity/GmailSharedMessage.svelte'
import Configure from './components/Configure.svelte'
import Connect from './components/Connect.svelte'
import IconGmail from './components/icons/GmailColor.svelte'
@ -37,7 +39,9 @@ export default async (): Promise<Resources> => ({
},
activity: {
TxSharedCreate,
TxWriteMessage
TxWriteMessage,
GmailWriteMessage,
GmailSharedMessage
},
function: {
HasEmail: checkHasEmail

View File

@ -22,24 +22,25 @@
import view, { Viewlet } from '@hcengineering/view'
import {
AnyComponent,
ButtonWithDropdown,
Component,
defineSeparators,
IconDropdown,
Label,
Loading,
location as locationStore,
Location,
Scroller,
Separator,
TabItem,
TabList,
Location,
IconDropdown,
ButtonWithDropdown
TabList
} from '@hcengineering/ui'
import chunter, { ThreadMessage } from '@hcengineering/chunter'
import { Ref, WithLookup } from '@hcengineering/core'
import { ViewletSelector } from '@hcengineering/view-resources'
import activity, { ActivityMessage } from '@hcengineering/activity'
import { isReactionMessage } from '@hcengineering/activity-resources'
import { get } from 'svelte/store'
import { inboxMessagesStore, InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
import Filter from '../Filter.svelte'
@ -99,6 +100,8 @@
let viewlet: WithLookup<Viewlet> | undefined
let loading = true
let selectedMessage: ActivityMessage | undefined = undefined
void client.findAll(notification.class.ActivityNotificationViewlet, {}).then((res) => {
viewlets = res
})
@ -136,6 +139,17 @@
if (selectedContextId !== selectedContext?._id) {
selectedContext = undefined
}
const selectedMessageId = loc?.loc.query?.message as Ref<ActivityMessage> | undefined
if (selectedMessageId !== undefined) {
selectedMessage = get(inboxClient.activityInboxNotifications).find(
({ attachedTo }) => attachedTo === selectedMessageId
)?.$lookup?.attachedTo
if (selectedMessage === undefined) {
selectedMessage = await client.findOne(activity.class.ActivityMessage, { _id: selectedMessageId })
}
}
}
$: selectedContext = selectedContextId
@ -368,6 +382,7 @@
_class: selectedContext.attachedToClass,
embedded: true,
context: selectedContext,
activityMessage: selectedMessage,
props: { context: selectedContext }
}}
on:close={() => selectContext(undefined)}

View File

@ -0,0 +1,51 @@
<!--
// 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 { createQuery, MessageViewer } from '@hcengineering/presentation'
import { TelegramMessage } from '@hcengineering/telegram'
import { Ref } from '@hcengineering/core'
import telegram from '../plugin'
export let _id: Ref<TelegramMessage> | undefined = undefined
export let value: TelegramMessage | undefined = undefined
const query = createQuery()
let doc: TelegramMessage | undefined = undefined
$: if (value === undefined && _id !== undefined) {
query.query(telegram.class.Message, { _id }, (res) => {
doc = res[0]
})
} else {
doc = value
query.unsubscribe()
}
</script>
{#if doc}
<div class="content lines-limit-2">
<MessageViewer message={doc.content} />
</div>
{/if}
<style lang="scss">
.content {
min-width: 0;
min-height: 1rem;
max-height: 2.125rem;
}
</style>

View File

@ -25,6 +25,7 @@ import TxMessage from './components/activity/TxMessage.svelte'
import IconTelegram from './components/icons/TelegramColor.svelte'
import TxSharedCreate from './components/activity/TxSharedCreate.svelte'
import TelegramMessageCreated from './components/activity/TelegramMessageCreated.svelte'
import MessagePresenter from './components/MessagePresenter.svelte'
import telegram from './plugin'
import { getCurrentEmployeeTG, getIntegrationOwnerTG } from './utils'
@ -36,7 +37,8 @@ export default async (): Promise<Resources> => ({
Connect,
Reconnect,
IconTelegram,
SharedMessages
SharedMessages,
MessagePresenter
},
activity: {
TxSharedCreate,

View File

@ -618,7 +618,7 @@ export function makeViewletKey (loc?: Location): string {
loc.query = undefined
// TODO: make better fix. Just temporary fix for correct inbox viewlets.
if (loc.path[2] === 'inbox') {
if (loc.path[2] === 'notification') {
loc.path = loc.path.slice(0, 3)
}