mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-25 17:42:57 +00:00
Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
commit
2acb183f58
@ -391,10 +391,27 @@ export function createModel (builder: Builder): void {
|
|||||||
_class: contact.mixin.Employee,
|
_class: contact.mixin.Employee,
|
||||||
icon: contact.icon.Person,
|
icon: contact.icon.Person,
|
||||||
label: contact.string.Employee,
|
label: contact.string.Employee,
|
||||||
|
baseQuery: {
|
||||||
|
role: { $ne: 'GUEST' }
|
||||||
|
},
|
||||||
createLabel: contact.string.CreateEmployee,
|
createLabel: contact.string.CreateEmployee,
|
||||||
createComponent: contact.component.CreateEmployee
|
createComponent: contact.component.CreateEmployee
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'guests',
|
||||||
|
component: workbench.component.SpecialView,
|
||||||
|
icon: contact.icon.Person,
|
||||||
|
label: contact.string.Guest,
|
||||||
|
componentProps: {
|
||||||
|
_class: contact.mixin.Employee,
|
||||||
|
icon: contact.icon.Person,
|
||||||
|
label: contact.string.Guest,
|
||||||
|
baseQuery: {
|
||||||
|
role: 'GUEST'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'persons',
|
id: 'persons',
|
||||||
component: workbench.component.SpecialView,
|
component: workbench.component.SpecialView,
|
||||||
|
@ -320,7 +320,8 @@ input.search {
|
|||||||
.flex-presenter, .inline-presenter {
|
.flex-presenter, .inline-presenter {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
cursor: pointer;
|
|
||||||
|
&:not(.no-pointer) { cursor: pointer; }
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
color: var(--theme-dark-color);
|
color: var(--theme-dark-color);
|
||||||
|
@ -128,18 +128,7 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
background-color: var(--theme-button-default);
|
background-color: var(--theme-button-default);
|
||||||
pointer-events: none;
|
// pointer-events: none;
|
||||||
|
|
||||||
&.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
pointer-events: auto;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.withStatus {
|
&.withStatus {
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
@ -211,6 +200,12 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Avatar sizes */
|
/* Avatar sizes */
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
"Everyone": "Všichni",
|
"Everyone": "Všichni",
|
||||||
"Here": "Zde",
|
"Here": "Zde",
|
||||||
"EveryoneDescription": "Upozornit všechny v tomto {title}",
|
"EveryoneDescription": "Upozornit všechny v tomto {title}",
|
||||||
"HereDescription": "Upozornit všechny v tomto {title}"
|
"HereDescription": "Upozornit všechny v tomto {title}",
|
||||||
|
"Guest": "Host"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
"Everyone": "Jeder",
|
"Everyone": "Jeder",
|
||||||
"Here": "Hier",
|
"Here": "Hier",
|
||||||
"EveryoneDescription": "Benachrichtigen Sie alle in diesem {title}",
|
"EveryoneDescription": "Benachrichtigen Sie alle in diesem {title}",
|
||||||
"HereDescription": "Benachrichtigen Sie alle in diesem {title}"
|
"HereDescription": "Benachrichtigen Sie alle in diesem {title}",
|
||||||
|
"Guest": "Gast"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"Everyone": "Everyone",
|
"Everyone": "Everyone",
|
||||||
"Here": "Here",
|
"Here": "Here",
|
||||||
"EveryoneDescription": "Notify everyone in this {title}",
|
"EveryoneDescription": "Notify everyone in this {title}",
|
||||||
"HereDescription": "Notify every online member in this {title}"
|
"HereDescription": "Notify every online member in this {title}",
|
||||||
|
"Guest": "Guest"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"Everyone": "Todos",
|
"Everyone": "Todos",
|
||||||
"Here": "Aquí",
|
"Here": "Aquí",
|
||||||
"EveryoneDescription": "Notificar a todos en este {title}",
|
"EveryoneDescription": "Notificar a todos en este {title}",
|
||||||
"HereDescription": "Notificar a todos en este {title}"
|
"HereDescription": "Notificar a todos en este {title}",
|
||||||
|
"Guest": "Invitado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"Everyone": "Tout le monde",
|
"Everyone": "Tout le monde",
|
||||||
"Here": "Ici",
|
"Here": "Ici",
|
||||||
"EveryoneDescription": "Notifier tout le monde dans ce {title}",
|
"EveryoneDescription": "Notifier tout le monde dans ce {title}",
|
||||||
"HereDescription": "Notifier tout le monde dans ce {title}"
|
"HereDescription": "Notifier tout le monde dans ce {title}",
|
||||||
|
"Guest": "Invité"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"Everyone": "Tutti",
|
"Everyone": "Tutti",
|
||||||
"Here": "Qui",
|
"Here": "Qui",
|
||||||
"EveryoneDescription": "Notifica tutti in questo {title}",
|
"EveryoneDescription": "Notifica tutti in questo {title}",
|
||||||
"HereDescription": "Notifica tutti in questo {title}"
|
"HereDescription": "Notifica tutti in questo {title}",
|
||||||
|
"Guest": "Ospite"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"Everyone": "全員",
|
"Everyone": "全員",
|
||||||
"Here": "ここ",
|
"Here": "ここ",
|
||||||
"EveryoneDescription": "この{title}のすべてに通知する",
|
"EveryoneDescription": "この{title}のすべてに通知する",
|
||||||
"HereDescription": "この{title}のすべてに通知する"
|
"HereDescription": "この{title}のすべてに通知する",
|
||||||
|
"Guest": "ゲスト"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -119,6 +119,7 @@
|
|||||||
"Everyone": "Todos",
|
"Everyone": "Todos",
|
||||||
"Here": "Aqui",
|
"Here": "Aqui",
|
||||||
"EveryoneDescription": "Notificar todos neste {title}",
|
"EveryoneDescription": "Notificar todos neste {title}",
|
||||||
"HereDescription": "Notificar todos neste {title}"
|
"HereDescription": "Notificar todos neste {title}",
|
||||||
|
"Guest": "Convidado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"Everyone": "Все",
|
"Everyone": "Все",
|
||||||
"Here": "Здесь",
|
"Here": "Здесь",
|
||||||
"EveryoneDescription": "Уведомить всех в этом {title}",
|
"EveryoneDescription": "Уведомить всех в этом {title}",
|
||||||
"HereDescription": "Уведомить всех в этом{title}"
|
"HereDescription": "Уведомить всех в этом{title}",
|
||||||
|
"Guest": "Гость"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"Everyone": "所有人",
|
"Everyone": "所有人",
|
||||||
"Here": "这里",
|
"Here": "这里",
|
||||||
"EveryoneDescription": "通知所有人在此{title}",
|
"EveryoneDescription": "通知所有人在此{title}",
|
||||||
"HereDescription": "通知所有人在此{title}"
|
"HereDescription": "通知所有人在此{title}",
|
||||||
|
"Guest": "访客"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="flex-presenter" use:tooltip={getPreviewPopup(person, showPreview)} on:click>
|
<div class="flex-presenter" class:no-pointer={!clickable} use:tooltip={getPreviewPopup(person, showPreview)} on:click>
|
||||||
{#if showStatus && person}
|
{#if showStatus && person}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<AvatarInstance
|
<AvatarInstance
|
||||||
@ -140,7 +140,6 @@
|
|||||||
{adaptiveName}
|
{adaptiveName}
|
||||||
{disabled}
|
{disabled}
|
||||||
{style}
|
{style}
|
||||||
{clickable}
|
|
||||||
withStatus
|
withStatus
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@ -164,13 +163,6 @@
|
|||||||
{adaptiveName}
|
{adaptiveName}
|
||||||
{disabled}
|
{disabled}
|
||||||
{style}
|
{style}
|
||||||
{clickable}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.flex-presenter {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -34,7 +34,6 @@
|
|||||||
export let adaptiveName: boolean = false
|
export let adaptiveName: boolean = false
|
||||||
export let disabled: boolean = false
|
export let disabled: boolean = false
|
||||||
export let style: 'modern' | undefined = undefined
|
export let style: 'modern' | undefined = undefined
|
||||||
export let clickable: boolean = false
|
|
||||||
|
|
||||||
function handleClick (): void {
|
function handleClick (): void {
|
||||||
dispatch('click')
|
dispatch('click')
|
||||||
@ -73,7 +72,7 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
class="hulyAvatar-container hulyAvatarSize-{size} {variant} {style} {clickable ? 'clickable' : ''}"
|
class="hulyAvatar-container hulyAvatarSize-{size} {variant} {style}"
|
||||||
class:no-img={!hasImg && color}
|
class:no-img={!hasImg && color}
|
||||||
class:bordered={!hasImg && color === undefined}
|
class:bordered={!hasImg && color === undefined}
|
||||||
class:border={bColor !== undefined}
|
class:border={bColor !== undefined}
|
||||||
@ -97,7 +96,7 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
class="hulyAvatar-container hulyAvatarSize-{size} {variant} {style} {clickable ? 'clickable' : ''}"
|
class="hulyAvatar-container hulyAvatarSize-{size} {variant} {style}"
|
||||||
class:no-img={!hasImg && color}
|
class:no-img={!hasImg && color}
|
||||||
class:bordered={!hasImg && color === undefined}
|
class:bordered={!hasImg && color === undefined}
|
||||||
class:border={bColor !== undefined}
|
class:border={bColor !== undefined}
|
||||||
@ -131,17 +130,3 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
|
||||||
.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.clickable > * {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.clickable img {
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@ -346,7 +346,8 @@ export const contactPlugin = plugin(contactId, {
|
|||||||
Everyone: '' as IntlString,
|
Everyone: '' as IntlString,
|
||||||
Here: '' as IntlString,
|
Here: '' as IntlString,
|
||||||
EveryoneDescription: '' as IntlString,
|
EveryoneDescription: '' as IntlString,
|
||||||
HereDescription: '' as IntlString
|
HereDescription: '' as IntlString,
|
||||||
|
Guest: '' as IntlString
|
||||||
},
|
},
|
||||||
viewlet: {
|
viewlet: {
|
||||||
TableMember: '' as Ref<Viewlet>,
|
TableMember: '' as Ref<Viewlet>,
|
||||||
|
@ -392,7 +392,7 @@ export async function fetchWorkspace (): Promise<[Status, WorkspaceInfoWithStatu
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workspaceWithStatus = await getAccountClient(token).getWorkspaceInfo()
|
const workspaceWithStatus = await getAccountClient(token).getWorkspaceInfo(true)
|
||||||
|
|
||||||
Analytics.handleEvent('Fetch workspace')
|
Analytics.handleEvent('Fetch workspace')
|
||||||
Analytics.setWorkspace(workspaceWithStatus.url)
|
Analytics.setWorkspace(workspaceWithStatus.url)
|
||||||
|
@ -154,6 +154,82 @@ describe('ChannelCache', () => {
|
|||||||
// Verify the cache doesn't contain the failed lookup
|
// Verify the cache doesn't contain the failed lookup
|
||||||
expect((channelCache as any).cache.has(`${spaceId}:${emailAccount}`)).toBe(false)
|
expect((channelCache as any).cache.has(`${spaceId}:${emailAccount}`)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not create duplicate channels for email addresses with different case', async () => {
|
||||||
|
// Arrange
|
||||||
|
const existingChannelId = 'mixed-case-channel-id'
|
||||||
|
const lowerCaseEmail = 'mixedcase@example.com'
|
||||||
|
const upperCaseEmail = 'MixedCase@Example.com'
|
||||||
|
|
||||||
|
// Mock first findOne to return null (channel doesn't exist yet)
|
||||||
|
// and second findOne to return the channel (after creation)
|
||||||
|
mockClient.findOne
|
||||||
|
.mockResolvedValueOnce(undefined) // First call: channel doesn't exist
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
// Second call after creation with different case
|
||||||
|
_id: existingChannelId as any,
|
||||||
|
title: lowerCaseEmail
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
mockClient.createDoc.mockResolvedValueOnce(existingChannelId as any)
|
||||||
|
|
||||||
|
// Act - First create a channel with lowercase email
|
||||||
|
const channelId1 = await channelCache.getOrCreateChannel(spaceId, participants, lowerCaseEmail, personId)
|
||||||
|
|
||||||
|
// Clear cache to simulate a fresh lookup
|
||||||
|
channelCache.clearCache(spaceId, lowerCaseEmail)
|
||||||
|
|
||||||
|
// Act - Then try to create with uppercase email
|
||||||
|
const channelId2 = await channelCache.getOrCreateChannel(spaceId, participants, upperCaseEmail, personId)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockClient.findOne).toHaveBeenNthCalledWith(1, mail.tag.MailChannel, { title: lowerCaseEmail })
|
||||||
|
expect(mockClient.findOne).toHaveBeenNthCalledWith(2, mail.tag.MailChannel, { title: lowerCaseEmail })
|
||||||
|
|
||||||
|
// Should only create doc once
|
||||||
|
expect(mockClient.createDoc).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockClient.createMixin).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// Both should return the same channel ID
|
||||||
|
expect(channelId1).toBe(existingChannelId)
|
||||||
|
expect(channelId2).toBe(existingChannelId)
|
||||||
|
expect(channelId1).toBe(channelId2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle race conditions when creating channels', async () => {
|
||||||
|
// Arrange - Simulate a race condition where channel is created between mutex lock and double-check
|
||||||
|
const raceChannelId = 'race-condition-channel-id'
|
||||||
|
|
||||||
|
// Mock behavior:
|
||||||
|
// 1. First findOne returns null (channel doesn't exist)
|
||||||
|
// 2. Second findOne (after mutex) returns a channel (someone else created it)
|
||||||
|
mockClient.findOne.mockResolvedValueOnce(undefined).mockResolvedValueOnce({
|
||||||
|
_id: raceChannelId,
|
||||||
|
title: 'race@example.com'
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const channelId = await channelCache.getOrCreateChannel(spaceId, participants, 'race@example.com', personId)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockClient.findOne).toHaveBeenCalledTimes(2)
|
||||||
|
expect(mockClient.createDoc).not.toHaveBeenCalled() // Should not create doc because of race check
|
||||||
|
expect(channelId).toBe(raceChannelId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should normalize email addresses to lowercase before lookup and creation', async () => {
|
||||||
|
// Arrange - This test verifies that email normalization to lowercase is implemented
|
||||||
|
|
||||||
|
const mixedCaseEmail = 'MiXeD@ExAmPlE.com'
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await channelCache.getOrCreateChannel(spaceId, participants, mixedCaseEmail, personId)
|
||||||
|
|
||||||
|
// Assert - If email normalization is implemented, this would pass
|
||||||
|
expect(mockClient.findOne).toHaveBeenCalledWith(mail.tag.MailChannel, {
|
||||||
|
title: expect.stringMatching(/mixed@example\.com/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('clearCache', () => {
|
describe('clearCache', () => {
|
||||||
|
@ -25,7 +25,7 @@ const createMutex = new SyncMutex()
|
|||||||
* Caches channel references to reduce calls to create mail channels
|
* Caches channel references to reduce calls to create mail channels
|
||||||
*/
|
*/
|
||||||
export class ChannelCache {
|
export class ChannelCache {
|
||||||
// Key is `${spaceId}:${emailAccount}`
|
// Key is `${spaceId}:${normalizedEmail}`
|
||||||
private readonly cache = new Map<string, Ref<Doc>>()
|
private readonly cache = new Map<string, Ref<Doc>>()
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
@ -40,17 +40,18 @@ export class ChannelCache {
|
|||||||
async getOrCreateChannel (
|
async getOrCreateChannel (
|
||||||
spaceId: Ref<PersonSpace>,
|
spaceId: Ref<PersonSpace>,
|
||||||
participants: PersonId[],
|
participants: PersonId[],
|
||||||
emailAccount: string,
|
email: string,
|
||||||
owner: PersonId
|
owner: PersonId
|
||||||
): Promise<Ref<Doc> | undefined> {
|
): Promise<Ref<Doc> | undefined> {
|
||||||
const cacheKey = `${spaceId}:${emailAccount}`
|
const normalizedEmail = normalizeEmail(email)
|
||||||
|
const cacheKey = `${spaceId}:${normalizedEmail}`
|
||||||
|
|
||||||
let channel = this.cache.get(cacheKey)
|
let channel = this.cache.get(cacheKey)
|
||||||
if (channel != null) {
|
if (channel != null) {
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
channel = await this.fetchOrCreateChannel(spaceId, participants, emailAccount, owner)
|
channel = await this.fetchOrCreateChannel(spaceId, participants, normalizedEmail, owner)
|
||||||
if (channel != null) {
|
if (channel != null) {
|
||||||
this.cache.set(cacheKey, channel)
|
this.cache.set(cacheKey, channel)
|
||||||
}
|
}
|
||||||
@ -58,8 +59,9 @@ export class ChannelCache {
|
|||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCache (spaceId: Ref<PersonSpace>, emailAccount: string): void {
|
clearCache (spaceId: Ref<PersonSpace>, email: string): void {
|
||||||
this.cache.delete(`${spaceId}:${emailAccount}`)
|
const normalizedEmail = normalizeEmail(email)
|
||||||
|
this.cache.delete(`${spaceId}:${normalizedEmail}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAllCache (): void {
|
clearAllCache (): void {
|
||||||
@ -73,29 +75,30 @@ export class ChannelCache {
|
|||||||
private async fetchOrCreateChannel (
|
private async fetchOrCreateChannel (
|
||||||
space: Ref<PersonSpace>,
|
space: Ref<PersonSpace>,
|
||||||
participants: PersonId[],
|
participants: PersonId[],
|
||||||
emailAccount: string,
|
email: string,
|
||||||
personId: PersonId
|
personId: PersonId
|
||||||
): Promise<Ref<Doc> | undefined> {
|
): Promise<Ref<Doc> | undefined> {
|
||||||
|
const normalizedEmail = normalizeEmail(email)
|
||||||
try {
|
try {
|
||||||
// First try to find existing channel
|
// First try to find existing channel
|
||||||
const channel = await this.client.findOne(mail.tag.MailChannel, { title: emailAccount })
|
const channel = await this.client.findOne(mail.tag.MailChannel, { title: normalizedEmail })
|
||||||
|
|
||||||
if (channel != null) {
|
if (channel != null) {
|
||||||
this.ctx.info('Using existing channel', { me: emailAccount, space, channel: channel._id })
|
this.ctx.info('Using existing channel', { me: normalizedEmail, space, channel: channel._id })
|
||||||
return channel._id
|
return channel._id
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.createNewChannel(space, participants, emailAccount, personId)
|
return await this.createNewChannel(space, participants, normalizedEmail, personId)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.ctx.error('Failed to create channel', {
|
this.ctx.error('Failed to create channel', {
|
||||||
me: emailAccount,
|
me: normalizedEmail,
|
||||||
space,
|
space,
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
error: err instanceof Error ? err.message : String(err)
|
error: err instanceof Error ? err.message : String(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Remove failed lookup from cache
|
// Remove failed lookup from cache
|
||||||
this.cache.delete(`${space}:${emailAccount}`)
|
this.cache.delete(`${space}:${normalizedEmail}`)
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@ -104,18 +107,19 @@ export class ChannelCache {
|
|||||||
private async createNewChannel (
|
private async createNewChannel (
|
||||||
space: Ref<PersonSpace>,
|
space: Ref<PersonSpace>,
|
||||||
participants: PersonId[],
|
participants: PersonId[],
|
||||||
emailAccount: string,
|
email: string,
|
||||||
personId: PersonId
|
personId: PersonId
|
||||||
): Promise<Ref<Doc> | undefined> {
|
): Promise<Ref<Doc> | undefined> {
|
||||||
const mutexKey = `channel:${this.workspace}:${space}:${emailAccount}`
|
const normalizedEmail = normalizeEmail(email)
|
||||||
|
const mutexKey = `channel:${this.workspace}:${space}:${normalizedEmail}`
|
||||||
const releaseLock = await createMutex.lock(mutexKey)
|
const releaseLock = await createMutex.lock(mutexKey)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Double-check that channel doesn't exist after acquiring lock
|
// Double-check that channel doesn't exist after acquiring lock
|
||||||
const existingChannel = await this.client.findOne(mail.tag.MailChannel, { title: emailAccount })
|
const existingChannel = await this.client.findOne(mail.tag.MailChannel, { title: normalizedEmail })
|
||||||
if (existingChannel != null) {
|
if (existingChannel != null) {
|
||||||
this.ctx.info('Using existing channel (found after mutex lock)', {
|
this.ctx.info('Using existing channel (found after mutex lock)', {
|
||||||
me: emailAccount,
|
me: normalizedEmail,
|
||||||
space,
|
space,
|
||||||
channel: existingChannel._id
|
channel: existingChannel._id
|
||||||
})
|
})
|
||||||
@ -123,12 +127,12 @@ export class ChannelCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new channel if it doesn't exist
|
// Create new channel if it doesn't exist
|
||||||
this.ctx.info('Creating new channel', { me: emailAccount, space, personId })
|
this.ctx.info('Creating new channel', { me: normalizedEmail, space, personId })
|
||||||
const channelId = await this.client.createDoc(
|
const channelId = await this.client.createDoc(
|
||||||
chat.masterTag.Channel,
|
chat.masterTag.Channel,
|
||||||
space,
|
space,
|
||||||
{
|
{
|
||||||
title: emailAccount,
|
title: normalizedEmail,
|
||||||
private: true,
|
private: true,
|
||||||
members: participants,
|
members: participants,
|
||||||
archived: false,
|
archived: false,
|
||||||
@ -140,7 +144,7 @@ export class ChannelCache {
|
|||||||
personId
|
personId
|
||||||
)
|
)
|
||||||
|
|
||||||
this.ctx.info('Creating mixin', { me: emailAccount, space, personId, channelId })
|
this.ctx.info('Creating mixin', { me: normalizedEmail, space, personId, channelId })
|
||||||
await this.client.createMixin(
|
await this.client.createMixin(
|
||||||
channelId,
|
channelId,
|
||||||
chat.masterTag.Channel,
|
chat.masterTag.Channel,
|
||||||
@ -187,3 +191,7 @@ export const ChannelCacheFactory = {
|
|||||||
return ChannelCacheFactory.instances.size
|
return ChannelCacheFactory.instances.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeEmail (email: string): string {
|
||||||
|
return email.toLowerCase().trim()
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user