Merge branch 'develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-01-23 00:00:05 +07:00
commit a0c9eaec85
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
34 changed files with 800 additions and 406 deletions

202
dev/tool/src/account.ts Normal file
View File

@ -0,0 +1,202 @@
import {
type Account,
type AccountDB,
changeEmail,
getAccount,
listWorkspacesPure,
type Workspace
} from '@hcengineering/account'
import core, { getWorkspaceId, type MeasureContext, systemAccountEmail, TxOperations } from '@hcengineering/core'
import contact from '@hcengineering/model-contact'
import { getTransactorEndpoint } from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
import { connect } from '@hcengineering/server-tool'
export async function renameAccount (
ctx: MeasureContext,
db: AccountDB,
accountsUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const account = await getAccount(db, oldEmail)
if (account == null) {
throw new Error("Account does'n exists")
}
const newAccount = await getAccount(db, newEmail)
if (newAccount != null) {
throw new Error('New Account email already exists:' + newAccount?.email + ' ' + newAccount?._id?.toString())
}
await changeEmail(ctx, db, account, newEmail)
await fixWorkspaceEmails(account, db, accountsUrl, oldEmail, newEmail)
}
export async function fixAccountEmails (
ctx: MeasureContext,
db: AccountDB,
transactorUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const account = await getAccount(db, newEmail)
if (account == null) {
throw new Error("Account does'n exists")
}
await fixWorkspaceEmails(account, db, transactorUrl, oldEmail, newEmail)
}
async function fixWorkspaceEmails (
account: Account,
db: AccountDB,
accountsUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const accountWorkspaces = account.workspaces.map((it) => it.toString())
// We need to update all workspaces
const workspaces = await listWorkspacesPure(db)
for (const ws of workspaces) {
if (!accountWorkspaces.includes(ws._id.toString())) {
continue
}
console.log('checking workspace', ws.workspaceName, ws.workspace)
const wsid = getWorkspaceId(ws.workspace)
const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid))
// Let's connect and update account information.
await fixEmailInWorkspace(endpoint, ws, oldEmail, newEmail)
}
}
async function fixEmailInWorkspace (
transactorUrl: string,
ws: Workspace,
oldEmail: string,
newEmail: string
): Promise<void> {
const connection = await connect(transactorUrl, { name: ws.workspace }, undefined, {
mode: 'backup',
model: 'upgrade', // Required for force all clients reload after operation will be complete.
admin: 'true'
})
try {
const personAccount = await connection.findOne(contact.class.PersonAccount, { email: oldEmail })
if (personAccount !== undefined) {
console.log('update account in ', ws.workspace)
const ops = new TxOperations(connection, core.account.ConfigUser)
await ops.update(personAccount, { email: newEmail })
}
} catch (err: any) {
console.error(err)
} finally {
await connection.close()
}
}
interface GithubUserResult {
login: string | null
code: number
rateLimitReset?: number | null
}
async function getGithubUser (githubId: string, ghToken?: string): Promise<GithubUserResult> {
const options =
ghToken !== undefined
? {
headers: {
Authorization: `Bearer ${ghToken}`,
Accept: 'application/vnd.github.v3+json'
}
}
: undefined
const res = await fetch(`https://api.github.com/user/${githubId}`, options)
if (res.status === 200) {
return {
login: (await res.json()).login,
code: 200
}
}
if (res.status === 403) {
const rateLimitReset = res.headers.get('X-RateLimit-Reset')
return {
login: null,
code: res.status,
rateLimitReset: rateLimitReset != null ? parseInt(rateLimitReset) * 1000 : null
}
}
return {
login: null,
code: res.status
}
}
export async function fillGithubUsers (ctx: MeasureContext, db: AccountDB, ghToken?: string): Promise<void> {
const githubAccounts = await db.account.find({ githubId: { $ne: null } })
if (githubAccounts.length === 0) {
ctx.info('no github accounts found')
return
}
const accountsToProcess = githubAccounts.filter(({ githubId, githubUser }) => githubUser == null && githubId != null)
if (accountsToProcess.length === 0) {
ctx.info('no github accounts left to fill')
return
}
ctx.info('processing github accounts', { total: accountsToProcess.length })
const defaultRetryTimeout = 1000 * 60 * 5 // 5 minutes
let processed = 0
for (const account of accountsToProcess) {
while (true) {
try {
if (account.githubId == null) break
let username: string | undefined
if (account.email.startsWith('github:')) {
username = account.email.slice(7)
} else {
const githubUserRes = await getGithubUser(account.githubId, ghToken)
if (githubUserRes.code === 200 && githubUserRes.login != null) {
username = githubUserRes.login
} else if (githubUserRes.code === 404) {
ctx.info('github user not found', { githubId: account.githubId })
break
} else if (githubUserRes.code === 403) {
const timeout =
githubUserRes.rateLimitReset != null
? githubUserRes.rateLimitReset - Date.now() + 1000
: defaultRetryTimeout
ctx.info('rate limit exceeded. Retrying in ', {
githubId: account.githubId,
retryTimeoutMin: Math.ceil(timeout / (1000 * 60))
})
await new Promise((resolve) => setTimeout(resolve, timeout))
} else {
ctx.error('failed to get github user', { githubId: account.githubId, ...githubUserRes })
break
}
}
if (username != null) {
await db.account.updateOne({ _id: account._id }, { githubUser: username.toLowerCase() })
ctx.info('github user added', { githubId: account.githubId, githubUser: username.toLowerCase() })
break
}
} catch (err: any) {
ctx.error('failed to fill github user', { githubId: account.githubId, err })
break
}
}
processed++
if (processed % 100 === 0) {
ctx.info('processing accounts:', { processed, of: accountsToProcess.length })
}
}
ctx.info('finished processing accounts:', { processed, of: accountsToProcess.length })
}

View File

@ -146,7 +146,7 @@ import {
} from './db'
import { restoreControlledDocContentMongo, restoreWikiContentMongo, restoreMarkupRefsMongo } from './markup'
import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin'
import { fixAccountEmails, renameAccount } from './renameAccount'
import { fixAccountEmails, renameAccount, fillGithubUsers } from './account'
import { copyToDatalake, moveFiles, showLostFiles } from './storage'
import { createPostgresTxAdapter, createPostgresAdapter, createPostgreeDestroyAdapter } from '@hcengineering/postgres'
import { reindexWorkspace } from './fulltext'
@ -2175,6 +2175,16 @@ export function devTool (
}
})
program
.command('fill-github-users')
.option('-t, --token <token>', 'Github token to increase the limit of requests to GitHub')
.description('adds github username info to all accounts')
.action(async (cmd: { token?: string }) => {
await withAccountDatabase(async (db) => {
await fillGithubUsers(toolCtx, db, cmd.token)
})
})
extendProgram?.(program)
program.parse(process.argv)

View File

@ -1,99 +0,0 @@
import {
type Account,
type AccountDB,
changeEmail,
getAccount,
listWorkspacesPure,
type Workspace
} from '@hcengineering/account'
import core, { getWorkspaceId, type MeasureContext, systemAccountEmail, TxOperations } from '@hcengineering/core'
import contact from '@hcengineering/model-contact'
import { getTransactorEndpoint } from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
import { connect } from '@hcengineering/server-tool'
export async function renameAccount (
ctx: MeasureContext,
db: AccountDB,
accountsUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const account = await getAccount(db, oldEmail)
if (account == null) {
throw new Error("Account does'n exists")
}
const newAccount = await getAccount(db, newEmail)
if (newAccount != null) {
throw new Error('New Account email already exists:' + newAccount?.email + ' ' + newAccount?._id?.toString())
}
await changeEmail(ctx, db, account, newEmail)
await fixWorkspaceEmails(account, db, accountsUrl, oldEmail, newEmail)
}
export async function fixAccountEmails (
ctx: MeasureContext,
db: AccountDB,
transactorUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const account = await getAccount(db, newEmail)
if (account == null) {
throw new Error("Account does'n exists")
}
await fixWorkspaceEmails(account, db, transactorUrl, oldEmail, newEmail)
}
async function fixWorkspaceEmails (
account: Account,
db: AccountDB,
accountsUrl: string,
oldEmail: string,
newEmail: string
): Promise<void> {
const accountWorkspaces = account.workspaces.map((it) => it.toString())
// We need to update all workspaces
const workspaces = await listWorkspacesPure(db)
for (const ws of workspaces) {
if (!accountWorkspaces.includes(ws._id.toString())) {
continue
}
console.log('checking workspace', ws.workspaceName, ws.workspace)
const wsid = getWorkspaceId(ws.workspace)
const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid))
// Let's connect and update account information.
await fixEmailInWorkspace(endpoint, ws, oldEmail, newEmail)
}
}
async function fixEmailInWorkspace (
transactorUrl: string,
ws: Workspace,
oldEmail: string,
newEmail: string
): Promise<void> {
const connection = await connect(transactorUrl, { name: ws.workspace }, undefined, {
mode: 'backup',
model: 'upgrade', // Required for force all clients reload after operation will be complete.
admin: 'true'
})
try {
const personAccount = await connection.findOne(contact.class.PersonAccount, { email: oldEmail })
if (personAccount !== undefined) {
console.log('update account in ', ws.workspace)
const ops = new TxOperations(connection, core.account.ConfigUser)
await ops.update(personAccount, { email: newEmail })
}
} catch (err: any) {
console.error(err)
} finally {
await connection.close()
}
}

View File

@ -239,6 +239,9 @@ export class TInboxNotification extends TDoc implements InboxNotification {
@Prop(TypeBoolean(), core.string.Boolean)
archived!: boolean
objectId!: Ref<Doc>
objectClass!: Ref<Class<Doc>>
declare space: Ref<PersonSpace>
title?: IntlString

View File

@ -247,6 +247,43 @@ export async function migrateSettings (client: MigrationClient): Promise<void> {
)
}
export async function migrateNotificationsObject (client: MigrationClient): Promise<void> {
while (true) {
const notifications = await client.find<InboxNotification>(
DOMAIN_NOTIFICATION,
{ objectId: { $exists: false }, docNotifyContext: { $exists: true } },
{ limit: 500 }
)
if (notifications.length === 0) return
const contextIds = Array.from(new Set(notifications.map((n) => n.docNotifyContext)))
const contexts = await client.find<DocNotifyContext>(DOMAIN_DOC_NOTIFY, { _id: { $in: contextIds } })
for (const context of contexts) {
await client.update(
DOMAIN_NOTIFICATION,
{ docNotifyContext: context._id, objectId: { $exists: false } },
{ objectId: context.objectId, objectClass: context.objectClass }
)
}
const toDelete: Ref<InboxNotification>[] = []
for (const notification of notifications) {
const context = contexts.find((c) => c._id === notification.docNotifyContext)
if (context === undefined) {
toDelete.push(notification._id)
}
}
if (toDelete.length > 0) {
await client.deleteMany(DOMAIN_NOTIFICATION, { _id: { $in: toDelete } })
}
}
}
export const notificationOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, notificationId, [
@ -429,6 +466,10 @@ export const notificationOperation: MigrateOperation = {
func: async (client) => {
await client.update(DOMAIN_DOC_NOTIFY, { space: core.space.Space }, { space: core.space.Workspace })
}
},
{
state: 'migrate-notifications-object',
func: migrateNotificationsObject
}
])
},

View File

@ -103,4 +103,13 @@ export function createModel (builder: Builder): void {
mixin: contact.mixin.Employee
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.PushNotificationsHandler,
isAsync: true,
txMatch: {
_class: core.class.TxCreateDoc,
objectClass: notification.class.InboxNotification
}
})
}

View File

@ -81,6 +81,8 @@
"Finished": "Dokončeno",
"StartWithRecording": "Začít s nahráváním",
"Language": "Jazyk",
"Kick": "Vyhodit"
"Kick": "Vyhodit",
"WithAudio": "S audio",
"ShareWithAudioTooltip": "Sdílet s audio, musíte znovu sdílet pro aplikování změn"
}
}

View File

@ -79,6 +79,8 @@
"Active": "Aktiv",
"Finished": "Beendet",
"StartWithRecording": "Mit Aufnahme starten",
"Language": "Sprache"
"Language": "Sprache",
"WithAudio": "Mit Audio",
"ShareWithAudioTooltip": "Mit Audio teilen, Sie müssen erneut teilen, um die Änderungen zu übernehmen"
}
}

View File

@ -81,6 +81,8 @@
"Finished": "Finished",
"StartWithRecording": "Start with recording",
"Language": "Language",
"Kick": "Kick"
"Kick": "Kick",
"WithAudio": "With audio",
"ShareWithAudioTooltip": "Share with audio, you should reshare for apply changes"
}
}

View File

@ -81,6 +81,8 @@
"Finished": "Terminado",
"StartWithRecording": "Iniciar con grabación",
"Language": "Idioma",
"Kick": "Expulsar"
"Kick": "Expulsar",
"WithAudio": "Con audio",
"ShareWithAudioTooltip": "Compartir con audio, debes compartir de nuevo para aplicar los cambios"
}
}

View File

@ -81,6 +81,8 @@
"Finished": "Terminé",
"StartWithRecording": "Démarrer avec l'enregistrement",
"Language": "Langue",
"Kick": "Expulser"
"Kick": "Expulser",
"WithAudio": "Avec audio",
"ShareWithAudioTooltip": "Partager avec audio, vous devez partager à nouveau pour appliquer les modifications"
}
}

View File

@ -81,6 +81,8 @@
"Finished": "Finito",
"StartWithRecording": "Inizia con la registrazione",
"Language": "Lingua",
"Kick": "Espellere"
"Kick": "Espellere",
"WithAudio": "Con audio",
"ShareWithAudioTooltip": "Condividi con audio, devi condividere nuovamente per applicare le modifiche"
}
}

View File

@ -81,6 +81,8 @@
"Finished": "Finalizado",
"StartWithRecording": "Começar com gravação",
"Language": "Idioma",
"Kick": "Expulsar"
"Kick": "Expulsar",
"WithAudio": "Com áudio",
"ShareWithAudioTooltip": "Compartilhar com áudio, você deve compartilhar novamente para aplicar as alterações"
}
}

View File

@ -81,6 +81,8 @@
"Finished": "Завершено",
"StartWithRecording": "Начинать с записью",
"Language": "Язык",
"Kick": "Выгнать"
"Kick": "Выгнать",
"WithAudio": "С аудио",
"ShareWithAudioTooltip": "Поделиться с аудио, вам нужно поделиться снова, чтобы применить изменения"
}
}

View File

@ -81,6 +81,8 @@
"Finished": "已完成",
"StartWithRecording": "开始录制",
"Language": "语言",
"Kick": "踢出"
"Kick": "踢出",
"WithAudio": "带音频",
"ShareWithAudioTooltip": "共享音频,您应重新共享以应用更改"
}
}

View File

@ -19,6 +19,10 @@
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import {
ButtonMenu,
DropdownIntlItem,
IconMaximize,
IconMoreV,
IconUpOutline,
ModernButton,
PopupInstance,
@ -26,11 +30,7 @@
eventToHTMLElement,
showPopup,
type AnySvelteComponent,
type CompAndProps,
IconMoreV,
ButtonMenu,
DropdownIntlItem,
IconMaximize
type CompAndProps
} from '@hcengineering/ui'
import view, { Action } from '@hcengineering/view'
import { getActions } from '@hcengineering/view-resources'
@ -38,30 +38,32 @@
import love from '../plugin'
import { currentRoom, myInfo, myOffice } from '../stores'
import {
isTranscriptionAllowed,
isCameraEnabled,
isConnected,
isFullScreen,
isMicEnabled,
isRecording,
isTranscription,
isRecordingAvailable,
isShareWithSound,
isSharingEnabled,
isTranscription,
isTranscriptionAllowed,
leaveRoom,
record,
screenSharing,
setCam,
setMic,
setShare,
stopTranscription,
startTranscription
startTranscription,
stopTranscription
} from '../utils'
import CamSettingPopup from './CamSettingPopup.svelte'
import ControlBarContainer from './ControlBarContainer.svelte'
import MicSettingPopup from './MicSettingPopup.svelte'
import RoomAccessPopup from './RoomAccessPopup.svelte'
import RoomLanguageSelector from './RoomLanguageSelector.svelte'
import ControlBarContainer from './ControlBarContainer.svelte'
import RoomModal from './RoomModal.svelte'
import ShareSettingPopup from './ShareSettingPopup.svelte'
export let room: Room
export let canMaximize: boolean = true
@ -86,7 +88,9 @@
}
async function changeShare (): Promise<void> {
await setShare(!$isSharingEnabled)
const newValue = !$isSharingEnabled
const audio = newValue && $isShareWithSound
await setShare(newValue, audio)
}
async function leave (): Promise<void> {
@ -122,6 +126,14 @@
}
}
function shareSettings (e: MouseEvent): void {
if (fullScreen) {
popup = getPopup(ShareSettingPopup, e)
} else {
showPopup(ShareSettingPopup, {}, eventToHTMLElement(e))
}
}
function setAccess (e: MouseEvent): void {
if (isOffice(room) && room.person !== me) return
if (fullScreen) {
@ -208,13 +220,15 @@
/>
{/if}
{#if allowShare}
<ModernButton
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
tooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
kind={'secondary'}
<SplitButton
size={'large'}
on:click={changeShare}
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
showTooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
action={changeShare}
secondIcon={IconUpOutline}
secondAction={shareSettings}
separate
/>
{/if}
{#if hasAccountRole(getCurrentAccount(), AccountRole.User) && $isRecordingAvailable}

View File

@ -14,43 +14,45 @@
-->
<script lang="ts">
import { Person, PersonAccount } from '@hcengineering/contact'
import { personByIdStore, UserInfo } from '@hcengineering/contact-resources'
import { IdMap, getCurrentAccount, Ref, Class, Doc } from '@hcengineering/core'
import {
ModernButton,
SplitButton,
IconArrowLeft,
IconUpOutline,
Label,
eventToHTMLElement,
Location,
location,
navigate,
showPopup,
Scroller,
panelstore
} from '@hcengineering/ui'
import { UserInfo, personByIdStore } from '@hcengineering/contact-resources'
import { Class, Doc, IdMap, Ref, getCurrentAccount } from '@hcengineering/core'
import {
MeetingMinutes,
ParticipantInfo,
Room,
RoomType,
isOffice,
loveId,
roomAccessIcon,
roomAccessLabel,
MeetingMinutes
roomAccessLabel
} from '@hcengineering/love'
import { createEventDispatcher } from 'svelte'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { getClient } from '@hcengineering/presentation'
import {
IconArrowLeft,
IconUpOutline,
Label,
Location,
ModernButton,
Scroller,
SplitButton,
eventToHTMLElement,
location,
navigate,
panelstore,
showPopup
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import love from '../plugin'
import { currentRoom, infos, invites, myInfo, myOffice, myRequests, currentMeetingMinutes, rooms } from '../stores'
import { currentMeetingMinutes, currentRoom, infos, invites, myInfo, myOffice, myRequests, rooms } from '../stores'
import {
endMeeting,
getRoomName,
isCameraEnabled,
isConnected,
isMicEnabled,
isShareWithSound,
isSharingEnabled,
leaveRoom,
screenSharing,
@ -62,7 +64,7 @@
import CamSettingPopup from './CamSettingPopup.svelte'
import MicSettingPopup from './MicSettingPopup.svelte'
import RoomAccessPopup from './RoomAccessPopup.svelte'
import view from '@hcengineering/view'
import ShareSettingPopup from './ShareSettingPopup.svelte'
export let room: Room
@ -99,7 +101,9 @@
}
async function changeShare (): Promise<void> {
await setShare(!$isSharingEnabled)
const newValue = !$isSharingEnabled
const audio = newValue && $isShareWithSound
await setShare(newValue, audio)
}
async function leave (): Promise<void> {
@ -114,6 +118,10 @@
dispatch('close')
}
function shareSettings (e: MouseEvent): void {
showPopup(ShareSettingPopup, {}, eventToHTMLElement(e))
}
async function connect (): Promise<void> {
await tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites)
dispatch('close')
@ -207,14 +215,15 @@
/>
{/if}
{#if allowShare}
<ModernButton
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
label={$isSharingEnabled ? love.string.StopShare : love.string.Share}
tooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
kind={'secondary'}
<SplitButton
size={'large'}
on:click={changeShare}
icon={$isSharingEnabled ? love.icon.SharingEnabled : love.icon.SharingDisabled}
showTooltip={{ label: $isSharingEnabled ? love.string.StopShare : love.string.Share }}
disabled={($screenSharing && !$isSharingEnabled) || !$isConnected}
action={changeShare}
secondIcon={IconUpOutline}
secondAction={shareSettings}
separate
/>
{/if}
<ModernButton

View File

@ -0,0 +1,30 @@
<script lang="ts">
import { Label, Toggle } from '@hcengineering/ui'
import love from '../plugin'
import { setShare, isShareWithSound, isSharingEnabled } from '../utils'
</script>
<div class="antiPopup p-4 grid">
<Label label={love.string.WithAudio} />
{$isShareWithSound}
<Toggle
showTooltip={{ label: love.string.ShareWithAudioTooltip }}
on={$isShareWithSound}
on:change={(e) => {
$isShareWithSound = e.detail
if ($isSharingEnabled) {
setShare($isSharingEnabled, e.detail)
}
}}
/>
</div>
<style lang="scss">
.grid {
display: grid;
grid-template-columns: 1fr auto;
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
}
</style>

View File

@ -13,9 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import aiBot from '@hcengineering/ai-bot'
import { Person, PersonAccount } from '@hcengineering/contact'
import { personIdByAccountId } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import { RoomType, Room as TypeRoom } from '@hcengineering/love'
import { MessageBox } from '@hcengineering/presentation'
import { ActionIcon, Scroller, showPopup } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { RoomType, Room as TypeRoom } from '@hcengineering/love'
import {
LocalParticipant,
LocalTrackPublication,
@ -28,11 +33,6 @@
TrackPublication
} from 'livekit-client'
import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte'
import { Ref } from '@hcengineering/core'
import { MessageBox } from '@hcengineering/presentation'
import { Person, PersonAccount } from '@hcengineering/contact'
import aiBot from '@hcengineering/ai-bot'
import { personIdByAccountId } from '@hcengineering/contact-resources'
import love from '../plugin'
import { currentRoom, infos, myInfo, myOffice } from '../stores'
@ -41,6 +41,7 @@
isCameraEnabled,
isConnected,
isMicEnabled,
isShareWithSound,
isSharingEnabled,
leaveRoom,
lk,
@ -289,7 +290,9 @@
async function changeShare (): Promise<void> {
if (!$isConnected) return
await setShare(!$isSharingEnabled)
const newValue = !$isSharingEnabled
const audio = newValue && $isShareWithSound
await setShare(newValue, audio)
}
$: dispatchFit($isSharingEnabled)

View File

@ -95,6 +95,8 @@ export default mergeIds(loveId, love, {
Select: '' as IntlString,
ChooseShare: '' as IntlString,
MoreOptions: '' as IntlString,
Language: '' as IntlString
Language: '' as IntlString,
WithAudio: '' as IntlString,
ShareWithAudioTooltip: '' as IntlString
}
})

View File

@ -77,6 +77,7 @@ import {
type RemoteTrack,
type RemoteTrackPublication,
RoomEvent,
type ScreenShareCaptureOptions,
Track,
type VideoCaptureOptions
} from 'livekit-client'
@ -174,6 +175,7 @@ export const isMicEnabled = writable<boolean>(false)
export const isCameraEnabled = writable<boolean>(false)
export const isSharingEnabled = writable<boolean>(false)
export const isFullScreen = writable<boolean>(false)
export const isShareWithSound = writable<boolean>(false)
function handleTrackSubscribed (
track: RemoteTrack,
@ -586,10 +588,14 @@ export async function setMic (value: boolean): Promise<void> {
}
}
export async function setShare (value: boolean): Promise<void> {
export async function setShare (value: boolean, withAudio: boolean = false): Promise<void> {
if ($isCurrentInstanceConnected) {
try {
await lk.localParticipant.setScreenShareEnabled(value)
const options: ScreenShareCaptureOptions = {}
if (withAudio) {
options.audio = true
}
await lk.localParticipant.setScreenShareEnabled(value, options)
} catch (err) {
console.error(err)
}

View File

@ -235,6 +235,8 @@ export interface InboxNotification extends Doc<PersonSpace> {
isViewed: boolean
docNotifyContext: Ref<DocNotifyContext>
objectId: Ref<Doc>
objectClass: Ref<Class<Doc>>
// For browser notifications
title?: IntlString

View File

@ -63,11 +63,12 @@ export function registerGithub (
async (ctx, next) => {
try {
let email = ctx.state.user.emails?.[0]?.value
const username = ctx.state.user.username.toLowerCase()
if (email == null || email === '') {
email = `github:${ctx.state.user.username}`
email = `github:${username}`
}
const [first, last] = ctx.state.user.displayName?.split(' ') ?? [ctx.state.user.username, '']
const [first, last] = ctx.state.user.displayName?.split(' ') ?? [username, '']
measureCtx.info('Provider auth handler', { email, type: 'github' })
if (email !== undefined) {
let loginInfo: LoginInfo | null
@ -76,7 +77,8 @@ export function registerGithub (
const db = await dbPromise
if (state.inviteId != null && state.inviteId !== '') {
loginInfo = await joinWithProvider(measureCtx, db, null, email, first, last, state.inviteId as any, {
githubId: ctx.state.user.id
githubId: ctx.state.user.id,
githubUser: username
})
} else {
loginInfo = await loginWithProvider(
@ -87,7 +89,8 @@ export function registerGithub (
first,
last,
{
githubId: ctx.state.user.id
githubId: ctx.state.user.id,
githubUser: username
},
signUpDisabled
)

View File

@ -151,6 +151,8 @@ export async function getPersonNotificationTxes (
messageHtml: reference.message,
mentionedIn: reference.attachedDocId ?? reference.srcDocId,
mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass,
objectId: reference.srcDocId,
objectClass: reference.srcDocClass,
user: receiver[0]._id,
isViewed: false,
archived: false
@ -238,9 +240,6 @@ export async function getPersonNotificationTxes (
modifiedOn: originTx.modifiedOn,
modifiedBy: sender._id
}
const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, {
user: receiverInfo._id
})
const msg = control.hierarchy.isDerived(data.mentionedInClass, activity.class.ActivityMessage)
? (await control.findAll(control.ctx, data.mentionedInClass, { _id: data.mentionedIn }))[0]
@ -248,14 +247,11 @@ export async function getPersonNotificationTxes (
await applyNotificationProviders(
notificationData,
notifyResult,
reference.srcDocId,
reference.srcDocClass,
control,
res,
doc,
receiverInfo,
senderInfo,
subscriptions,
notification.class.MentionInboxNotification,
msg as ActivityMessage
)

View File

@ -15,16 +15,8 @@
//
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics'
import chunter, { ChatMessage } from '@hcengineering/chunter'
import contact, {
Employee,
getAvatarProviderId,
getGravatarUrl,
Person,
PersonAccount,
type AvatarInfo
} from '@hcengineering/contact'
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
import core, {
Account,
AnyAttribute,
@ -33,7 +25,6 @@ import core, {
Class,
Collection,
combineAttributes,
concatLink,
Data,
Doc,
DocumentUpdate,
@ -64,26 +55,28 @@ import notification, {
DocNotifyContext,
InboxNotification,
MentionInboxNotification,
notificationId,
NotificationType,
PushData,
PushSubscription
NotificationType
} from '@hcengineering/notification'
import { getMetadata, getResource, translate } from '@hcengineering/platform'
import serverCore, { type TriggerControl } from '@hcengineering/server-core'
import { getResource, translate } from '@hcengineering/platform'
import { type TriggerControl } from '@hcengineering/server-core'
import serverNotification, {
getPersonAccountById,
NOTIFICATION_BODY_SIZE,
PUSH_NOTIFICATION_TITLE_SIZE,
ReceiverInfo,
SenderInfo
} from '@hcengineering/server-notification'
import serverView from '@hcengineering/server-view'
import { markupToText, stripTags } from '@hcengineering/text-core'
import { encodeObjectURI } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import { Analytics } from '@hcengineering/analytics'
import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types'
import {
AvailableProvidersCache,
AvailableProvidersCacheKey,
Content,
ContextsCache,
ContextsCacheKey,
NotifyParams,
NotifyResult
} from './types'
import {
createPullCollaboratorsTx,
createPushCollaboratorsTx,
@ -105,6 +98,7 @@ import {
updateNotifyContextsSpace,
type NotificationProviderControl
} from './utils'
import { PushNotificationsHandler } from './push'
export function getPushCollaboratorTx (
control: TriggerControl,
@ -165,20 +159,8 @@ export async function getCommonNotificationTxes (
if (notificationTx !== undefined) {
const notificationData = TxProcessor.createDoc2Doc(notificationTx)
const subscriptions = await control.findAll(ctx, notification.class.PushSubscription, { user: receiver._id })
await applyNotificationProviders(
notificationData,
notifyResult,
attachedTo,
attachedToClass,
control,
res,
doc,
receiver,
sender,
subscriptions,
_class
)
await applyNotificationProviders(notificationData, notifyResult, control, res, doc, receiver, sender, _class)
}
return res
@ -383,6 +365,8 @@ export async function pushInboxNotifications (
isViewed: false,
docNotifyContext: docNotifyContextId,
archived: false,
objectId,
objectClass,
...data
}
const notificationTx = control.txFactory.createTxCreateDoc(_class, receiver.space, notificationData)
@ -489,156 +473,6 @@ export async function getTranslatedNotificationContent (
return { title: '', body: '' }
}
function isReactionMessage (message?: ActivityMessage): boolean {
return (
message !== undefined &&
message._class === activity.class.DocUpdateMessage &&
(message as DocUpdateMessage).objectClass === activity.class.Reaction
)
}
export async function createPushFromInbox (
control: TriggerControl,
receiver: ReceiverInfo,
attachedTo: Ref<Doc>,
attachedToClass: Ref<Class<Doc>>,
data: Data<InboxNotification>,
_class: Ref<Class<InboxNotification>>,
sender: SenderInfo,
_id: Ref<Doc>,
subscriptions: PushSubscription[],
message?: ActivityMessage
): Promise<Tx | undefined> {
let { title, body } = await getTranslatedNotificationContent(data, _class, control)
if (title === '' || body === '') {
return
}
title = title.slice(0, PUSH_NOTIFICATION_TITLE_SIZE)
const senderPerson = sender.person
const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {})
const provider = linkProviders.find(({ _id }) => _id === attachedToClass)
let id: string = attachedTo
if (provider !== undefined) {
const encodeFn = await getResource(provider.encode)
const doc = (await control.findAll(control.ctx, attachedToClass, { _id: attachedTo }))[0]
if (doc === undefined) {
return
}
id = await encodeFn(doc, control)
}
const path = [workbenchId, control.workspace.workspaceUrl, notificationId, encodeObjectURI(id, attachedToClass)]
await createPushNotification(
control,
receiver._id as Ref<PersonAccount>,
title,
body,
_id,
subscriptions,
senderPerson,
path
)
return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiver.space, {
user: receiver._id,
title,
body,
senderId: sender._id,
tag: _id,
objectId: attachedTo,
objectClass: attachedToClass,
messageId: isReactionMessage(message) ? (message?.attachedTo as Ref<ActivityMessage>) : message?._id,
messageClass: isReactionMessage(message)
? (message?.attachedToClass as Ref<Class<ActivityMessage>>)
: message?._class,
onClickLocation: {
path
}
})
}
export async function createPushNotification (
control: TriggerControl,
target: Ref<PersonAccount>,
title: string,
body: string,
_id: string,
subscriptions: PushSubscription[],
senderAvatar?: Data<AvatarInfo>,
path?: string[]
): Promise<void> {
const sesURL: string | undefined = getMetadata(serverNotification.metadata.SesUrl)
const sesAuth: string | undefined = getMetadata(serverNotification.metadata.SesAuthToken)
if (sesURL === undefined || sesURL === '') return
const userSubscriptions = subscriptions.filter((it) => it.user === target)
const data: PushData = {
title,
body
}
if (_id !== undefined) {
data.tag = _id
}
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}`
data.domain = concatLink(front, domainPath)
if (path !== undefined) {
data.url = concatLink(front, path.join('/'))
}
if (senderAvatar != null) {
const provider = getAvatarProviderId(senderAvatar.avatarType)
if (provider === contact.avatarProvider.Image) {
if (senderAvatar.avatar != null) {
const url = await control.storageAdapter.getUrl(control.ctx, control.workspace, senderAvatar.avatar)
data.icon = url.includes('://') ? url : concatLink(front, url)
}
} else if (provider === contact.avatarProvider.Gravatar && senderAvatar.avatarProps?.url !== undefined) {
data.icon = getGravatarUrl(senderAvatar.avatarProps?.url, 512)
}
}
void sendPushToSubscription(sesURL, sesAuth, control, target, userSubscriptions, data)
}
async function sendPushToSubscription (
sesURL: string,
sesAuth: string | undefined,
control: TriggerControl,
targetUser: Ref<Account>,
subscriptions: PushSubscription[],
data: PushData
): Promise<void> {
try {
const result: Ref<PushSubscription>[] = (
await (
await fetch(concatLink(sesURL, '/web-push'), {
method: 'post',
headers: {
'Content-Type': 'application/json',
...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {})
},
body: JSON.stringify({
subscriptions,
data
})
})
).json()
).result
if (result.length > 0) {
const domain = control.hierarchy.findDomain(notification.class.PushSubscription)
if (domain !== undefined) {
await control.lowLevel.clean(control.ctx, domain, result)
}
}
} catch (err) {
control.ctx.info('Cannot send push notification to', { user: targetUser, err })
}
}
/**
* @public
*/
@ -682,40 +516,16 @@ export async function pushActivityInboxNotifications (
export async function applyNotificationProviders (
data: InboxNotification,
notifyResult: NotifyResult,
attachedTo: Ref<Doc>,
attachedToClass: Ref<Class<Doc>>,
control: TriggerControl,
res: Tx[],
object: Doc,
receiver: ReceiverInfo,
sender: SenderInfo,
subscriptions: PushSubscription[],
_class = notification.class.ActivityInboxNotification,
message?: ActivityMessage
): Promise<void> {
const resources = control.modelDb.findAllSync(serverNotification.class.NotificationProviderResources, {})
for (const [provider, types] of notifyResult.entries()) {
if (provider === notification.providers.PushNotificationProvider) {
// const now = Date.now()
const pushTx = await createPushFromInbox(
control,
receiver,
attachedTo,
attachedToClass,
data,
_class,
sender,
data._id,
subscriptions,
message
)
if (pushTx !== undefined) {
res.push(pushTx)
}
continue
}
const resource = resources.find((it) => it.provider === provider)
if (resource === undefined) continue
@ -789,8 +599,7 @@ export async function getNotificationTxes (
params: NotifyParams,
docNotifyContexts: DocNotifyContext[],
activityMessages: ActivityMessage[],
settings: NotificationProviderControl,
subscriptions: PushSubscription[]
settings: NotificationProviderControl
): Promise<Tx[]> {
if (receiver.account === undefined) {
return []
@ -828,17 +637,23 @@ export async function getNotificationTxes (
if (notificationTx !== undefined) {
const notificationData = TxProcessor.createDoc2Doc(notificationTx)
const current: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map()
const providers = Array.from(notifyResult.keys()).filter(
(p) => p !== notification.providers.InboxNotificationProvider
)
if (providers.length > 0) {
current.set(notificationData._id, providers)
control.contextCache.set('AvailableNotificationProviders', current)
}
await applyNotificationProviders(
notificationData,
notifyResult,
message.attachedTo,
message.attachedToClass,
control,
res,
object,
receiver,
sender,
subscriptions,
notificationData._class,
message
)
@ -1030,9 +845,6 @@ export async function createCollabDocInfo (
}
const settings = await getNotificationProviderControl(ctx, control)
const subscriptions = (await control.queryFind(ctx, notification.class.PushSubscription, {})).filter((it) =>
targets.has(it.user as Ref<PersonAccount>)
)
for (const target of targets) {
const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target))
@ -1049,8 +861,7 @@ export async function createCollabDocInfo (
params,
notifyContexts,
docMessages,
settings,
subscriptions
settings
)
const ids = new Set(targetRes.map((it) => it._id))
if (info.account?.email !== undefined) {
@ -2031,6 +1842,7 @@ async function OnDocRemove (txes: TxCUD<Doc>[], control: TriggerControl): Promis
}
export * from './types'
export * from './push'
export * from './utils'
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@ -2039,7 +1851,8 @@ export default async () => ({
OnAttributeCreate,
OnAttributeUpdate,
OnDocRemove,
OnEmployeeDeactivate
OnEmployeeDeactivate,
PushNotificationsHandler
},
function: {
IsUserInFieldValueTypeMatch: isUserInFieldValueTypeMatch,

View File

@ -0,0 +1,295 @@
//
// Copyright © 2025 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 serverCore, { TriggerControl } from '@hcengineering/server-core'
import serverNotification, { PUSH_NOTIFICATION_TITLE_SIZE } from '@hcengineering/server-notification'
import {
Account,
Class,
concatLink,
Data,
Doc,
Hierarchy,
Ref,
Tx,
TxCreateDoc,
TxProcessor
} from '@hcengineering/core'
import notification, {
ActivityInboxNotification,
InboxNotification,
MentionInboxNotification,
notificationId,
PushData,
PushSubscription
} from '@hcengineering/notification'
import activity, { ActivityMessage } from '@hcengineering/activity'
import serverView from '@hcengineering/server-view'
import { getMetadata, getResource } from '@hcengineering/platform'
import { workbenchId } from '@hcengineering/workbench'
import { encodeObjectURI } from '@hcengineering/view'
import contact, {
type AvatarInfo,
getAvatarProviderId,
getGravatarUrl,
Person,
PersonAccount,
PersonSpace
} from '@hcengineering/contact'
import { AvailableProvidersCache, AvailableProvidersCacheKey, getTranslatedNotificationContent } from './index'
async function createPushFromInbox (
control: TriggerControl,
n: InboxNotification,
receiver: Ref<Account>,
receiverSpace: Ref<PersonSpace>,
subscriptions: PushSubscription[],
senderPerson?: Person
): Promise<Tx | undefined> {
let { title, body } = await getTranslatedNotificationContent(n, n._class, control)
if (title === '' || body === '') {
return
}
title = title.slice(0, PUSH_NOTIFICATION_TITLE_SIZE)
const linkProviders = control.modelDb.findAllSync(serverView.mixin.ServerLinkIdProvider, {})
const provider = linkProviders.find(({ _id }) => _id === n.objectClass)
let id: string = n.objectId
if (provider !== undefined) {
const encodeFn = await getResource(provider.encode)
const cache: Map<Ref<Doc>, Doc> = control.contextCache.get('PushNotificationsHandler') ?? new Map()
const doc = cache.get(n.objectId) ?? (await control.findAll(control.ctx, n.objectClass, { _id: n.objectId }))[0]
if (doc === undefined) {
return
}
cache.set(n.objectId, doc)
control.contextCache.set('PushNotificationsHandler', cache)
id = await encodeFn(doc, control)
}
const path = [workbenchId, control.workspace.workspaceUrl, notificationId, encodeObjectURI(id, n.objectClass)]
await createPushNotification(
control,
receiver as Ref<PersonAccount>,
title,
body,
n._id,
subscriptions,
senderPerson,
path
)
const messageInfo = getMessageInfo(n, control.hierarchy)
return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiverSpace, {
user: receiver,
title,
body,
senderId: n.createdBy ?? n.modifiedBy,
tag: n._id,
objectId: n.objectId,
objectClass: n.objectClass,
messageId: messageInfo._id,
messageClass: messageInfo._class,
onClickLocation: {
path
}
})
}
function getMessageInfo (
n: InboxNotification,
hierarchy: Hierarchy
): {
_id?: Ref<ActivityMessage>
_class?: Ref<Class<ActivityMessage>>
} {
if (hierarchy.isDerived(n._class, notification.class.ActivityInboxNotification)) {
const activityNotification = n as ActivityInboxNotification
if (
activityNotification.attachedToClass === activity.class.DocUpdateMessage &&
hierarchy.isDerived(activityNotification.objectClass, activity.class.ActivityMessage)
) {
return {
_id: activityNotification.objectId as Ref<ActivityMessage>,
_class: activityNotification.objectClass
}
}
return {
_id: activityNotification.attachedTo,
_class: activityNotification.attachedToClass
}
}
if (hierarchy.isDerived(n._class, notification.class.MentionInboxNotification)) {
const mentionNotification = n as MentionInboxNotification
if (hierarchy.isDerived(mentionNotification.mentionedInClass, activity.class.ActivityMessage)) {
return {
_id: mentionNotification.mentionedIn as Ref<ActivityMessage>,
_class: mentionNotification.mentionedInClass
}
}
}
return {}
}
export async function createPushNotification (
control: TriggerControl,
target: Ref<PersonAccount>,
title: string,
body: string,
_id: string,
subscriptions: PushSubscription[],
senderAvatar?: Data<AvatarInfo>,
path?: string[]
): Promise<void> {
const sesURL: string | undefined = getMetadata(serverNotification.metadata.SesUrl)
const sesAuth: string | undefined = getMetadata(serverNotification.metadata.SesAuthToken)
if (sesURL === undefined || sesURL === '') return
const userSubscriptions = subscriptions.filter((it) => it.user === target)
const data: PushData = {
title,
body
}
if (_id !== undefined) {
data.tag = _id
}
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
const domainPath = `${workbenchId}/${control.workspace.workspaceUrl}`
data.domain = concatLink(front, domainPath)
if (path !== undefined) {
data.url = concatLink(front, path.join('/'))
}
if (senderAvatar != null) {
const provider = getAvatarProviderId(senderAvatar.avatarType)
if (provider === contact.avatarProvider.Image) {
if (senderAvatar.avatar != null) {
const url = await control.storageAdapter.getUrl(control.ctx, control.workspace, senderAvatar.avatar)
data.icon = url.includes('://') ? url : concatLink(front, url)
}
} else if (provider === contact.avatarProvider.Gravatar && senderAvatar.avatarProps?.url !== undefined) {
data.icon = getGravatarUrl(senderAvatar.avatarProps?.url, 512)
}
}
void sendPushToSubscription(sesURL, sesAuth, control, target, userSubscriptions, data)
}
async function sendPushToSubscription (
sesURL: string,
sesAuth: string | undefined,
control: TriggerControl,
targetUser: Ref<Account>,
subscriptions: PushSubscription[],
data: PushData
): Promise<void> {
try {
const result: Ref<PushSubscription>[] = (
await (
await fetch(concatLink(sesURL, '/web-push'), {
method: 'post',
headers: {
'Content-Type': 'application/json',
...(sesAuth != null ? { Authorization: `Bearer ${sesAuth}` } : {})
},
body: JSON.stringify({
subscriptions,
data
})
})
).json()
).result
if (result.length > 0) {
const domain = control.hierarchy.findDomain(notification.class.PushSubscription)
if (domain !== undefined) {
await control.lowLevel.clean(control.ctx, domain, result)
}
}
} catch (err) {
control.ctx.info('Cannot send push notification to', { user: targetUser, err })
}
}
export async function PushNotificationsHandler (
txes: TxCreateDoc<InboxNotification>[],
control: TriggerControl
): Promise<Tx[]> {
const availableProviders: AvailableProvidersCache = control.contextCache.get(AvailableProvidersCacheKey) ?? new Map()
const all: InboxNotification[] = txes
.map((tx) => TxProcessor.createDoc2Doc(tx))
.filter(
(it) =>
availableProviders.get(it._id)?.find((p) => p === notification.providers.PushNotificationProvider) !== undefined
)
if (all.length === 0) {
return []
}
const receivers = new Set(all.map((it) => it.user))
const subscriptions = (await control.queryFind(control.ctx, notification.class.PushSubscription, {})).filter((it) =>
receivers.has(it.user)
)
if (subscriptions.length === 0) {
return []
}
const senders = Array.from(new Set(all.map((it) => it.createdBy)))
const senderAccounts = await control.modelDb.findAll(contact.class.PersonAccount, {
_id: { $in: senders as Ref<PersonAccount>[] }
})
const senderPersons = await control.findAll(control.ctx, contact.class.Person, {
_id: { $in: Array.from(new Set(senderAccounts.map((it) => it.person))) }
})
const res: Tx[] = []
for (const inboxNotification of all) {
const { user } = inboxNotification
const userSubscriptions = subscriptions.filter((it) => it.user === user)
if (userSubscriptions.length === 0) continue
const senderAccount = senderAccounts.find(
(it) => it._id === (inboxNotification.createdBy ?? inboxNotification.modifiedBy)
)
const senderPerson =
senderAccount !== undefined ? senderPersons.find((it) => it._id === senderAccount.person) : undefined
const tx = await createPushFromInbox(
control,
inboxNotification,
user,
inboxNotification.space,
userSubscriptions,
senderPerson
)
if (tx !== undefined) {
res.push(tx)
}
}
return res
}

View File

@ -12,7 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { BaseNotificationType, DocNotifyContext, NotificationProvider } from '@hcengineering/notification'
import {
BaseNotificationType,
DocNotifyContext,
InboxNotification,
NotificationProvider
} from '@hcengineering/notification'
import { Ref } from '@hcengineering/core'
/**
@ -39,3 +44,6 @@ export const ContextsCacheKey = 'DocNotifyContexts'
export interface ContextsCache {
contexts: Map<string, Ref<DocNotifyContext>>
}
export const AvailableProvidersCacheKey = 'AvailableNotificationProviders'
export type AvailableProvidersCache = Map<Ref<InboxNotification>, Ref<NotificationProvider>[]>

View File

@ -661,3 +661,11 @@ export async function getObjectSpace (control: TriggerControl, doc: Doc, cache:
: (cache.get(doc.space) as Space) ??
(await control.findAll<Space>(control.ctx, core.class.Space, { _id: doc.space }, { limit: 1 }))[0]
}
export function isReactionMessage (message?: ActivityMessage): boolean {
return (
message !== undefined &&
message._class === activity.class.DocUpdateMessage &&
(message as DocUpdateMessage).objectClass === activity.class.Reaction
)
}

View File

@ -168,7 +168,8 @@ export default plugin(serverNotificationId, {
OnAttributeUpdate: '' as Resource<TriggerFunc>,
OnReactionChanged: '' as Resource<TriggerFunc>,
OnDocRemove: '' as Resource<TriggerFunc>,
OnEmployeeDeactivate: '' as Resource<TriggerFunc>
OnEmployeeDeactivate: '' as Resource<TriggerFunc>,
PushNotificationsHandler: '' as Resource<TriggerFunc>
},
function: {
IsUserInFieldValueTypeMatch: '' as TypeMatchFunc,

View File

@ -161,10 +161,7 @@ async function getRequestNotificationTx (
}
const notificationControl = await getNotificationProviderControl(ctx, control)
const collaboratorsSet = new Set(collaborators)
const subscriptions = (await control.queryFind(control.ctx, notification.class.PushSubscription, {})).filter((it) =>
collaboratorsSet.has(it.user)
)
for (const target of collaborators) {
const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target))
if (targetInfo === undefined) continue
@ -179,8 +176,7 @@ async function getRequestNotificationTx (
{ isOwn: true, isSpace: false, shouldUpdateTimestamp: true },
notifyContexts,
messages,
notificationControl,
subscriptions
notificationControl
)
res.push(...txes)
}

View File

@ -89,6 +89,12 @@ export class PostgresDbCollection<T extends Record<string, any>> implements DbCo
values.push(Object.values(qKey as object)[0])
break
}
case '$ne': {
currIdx++
whereChunks.push(`"${key}" != $${currIdx}`)
values.push(Object.values(qKey as object)[0])
break
}
default: {
currIdx++
whereChunks.push(`"${key}" = $${currIdx}`)

View File

@ -2678,7 +2678,14 @@ export async function joinWithProvider (
}
let account = await getAccount(db, email)
if (account == null && extra !== undefined) {
account = await getAccountByQuery(db, extra)
// Temporary: we don't want to use githubUser to search yet
// but we want to save it in the account
const extraSearch = {
...extra
}
delete extraSearch.githubUser
account = await getAccountByQuery(db, extraSearch)
}
if (account !== null) {
// we should clean password if account is not confirmed
@ -2760,7 +2767,14 @@ export async function loginWithProvider (
}
let account = await getAccount(db, email)
if (account == null && extra !== undefined) {
account = await getAccountByQuery(db, extra)
// Temporary: we don't want to use githubUser to search yet
// but we want to save it in the account
const extraSearch = {
...extra
}
delete extraSearch.githubUser
account = await getAccountByQuery(db, extraSearch)
}
if (account !== null) {
// we should clean password if account is not confirmed

View File

@ -46,6 +46,7 @@ export interface Account {
createdOn: number
lastVisit: number
githubId?: string
githubUser?: string
openId?: string
}
@ -146,6 +147,7 @@ export interface UpgradeStatistic {
interface Operator<T, P extends keyof T> {
$in?: T[P][]
$ne?: T[P] | null
$lt?: T[P]
$lte?: T[P]
$gt?: T[P]

View File

@ -36,6 +36,8 @@ export async function createNotification (
} else {
await client.createDoc(notification.class.CommonInboxNotification, data.space, {
user: data.user,
objectId: forDoc._id,
objectClass: forDoc._class,
icon: github.icon.Github,
message: data.message,
props: data.props,