diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts
index 4c4fad48ac..b2dc6af6d6 100644
--- a/models/contact/src/index.ts
+++ b/models/contact/src/index.ts
@@ -391,10 +391,27 @@ export function createModel (builder: Builder): void {
_class: contact.mixin.Employee,
icon: contact.icon.Person,
label: contact.string.Employee,
+ baseQuery: {
+ role: { $ne: 'GUEST' }
+ },
createLabel: contact.string.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',
component: workbench.component.SpecialView,
diff --git a/packages/theme/styles/_layouts.scss b/packages/theme/styles/_layouts.scss
index a6986f578e..f0220b81c6 100644
--- a/packages/theme/styles/_layouts.scss
+++ b/packages/theme/styles/_layouts.scss
@@ -320,7 +320,8 @@ input.search {
.flex-presenter, .inline-presenter {
flex-wrap: nowrap;
min-width: 0;
- cursor: pointer;
+
+ &:not(.no-pointer) { cursor: pointer; }
.icon {
color: var(--theme-dark-color);
diff --git a/packages/theme/styles/components.scss b/packages/theme/styles/components.scss
index 0d5e1f34fc..84624d3e26 100644
--- a/packages/theme/styles/components.scss
+++ b/packages/theme/styles/components.scss
@@ -128,18 +128,7 @@
flex-shrink: 0;
aspect-ratio: 1;
background-color: var(--theme-button-default);
- pointer-events: none;
-
- &.clickable {
- cursor: pointer;
- pointer-events: auto;
-
- & > * {
- pointer-events: none;
- user-select: none;
- -webkit-user-drag: none;
- }
- }
+ // pointer-events: none;
&.withStatus {
mask-repeat: no-repeat;
@@ -211,6 +200,12 @@
transform: translate(-50%, -50%);
}
}
+
+ & > * {
+ pointer-events: none;
+ user-select: none;
+ -webkit-user-drag: none;
+ }
}
/* Avatar sizes */
diff --git a/plugins/contact-assets/lang/cs.json b/plugins/contact-assets/lang/cs.json
index b5a64f7e54..186423a94a 100644
--- a/plugins/contact-assets/lang/cs.json
+++ b/plugins/contact-assets/lang/cs.json
@@ -115,6 +115,7 @@
"Everyone": "Všichni",
"Here": "Zde",
"EveryoneDescription": "Upozornit všechny v tomto {title}",
- "HereDescription": "Upozornit všechny v tomto {title}"
+ "HereDescription": "Upozornit všechny v tomto {title}",
+ "Guest": "Host"
}
}
diff --git a/plugins/contact-assets/lang/de.json b/plugins/contact-assets/lang/de.json
index 426387502c..6f593eb0bd 100644
--- a/plugins/contact-assets/lang/de.json
+++ b/plugins/contact-assets/lang/de.json
@@ -115,6 +115,7 @@
"Everyone": "Jeder",
"Here": "Hier",
"EveryoneDescription": "Benachrichtigen Sie alle in diesem {title}",
- "HereDescription": "Benachrichtigen Sie alle in diesem {title}"
+ "HereDescription": "Benachrichtigen Sie alle in diesem {title}",
+ "Guest": "Gast"
}
}
diff --git a/plugins/contact-assets/lang/en.json b/plugins/contact-assets/lang/en.json
index 805a85c281..4ef79dfcf9 100644
--- a/plugins/contact-assets/lang/en.json
+++ b/plugins/contact-assets/lang/en.json
@@ -119,6 +119,7 @@
"Everyone": "Everyone",
"Here": "Here",
"EveryoneDescription": "Notify everyone in this {title}",
- "HereDescription": "Notify every online member in this {title}"
+ "HereDescription": "Notify every online member in this {title}",
+ "Guest": "Guest"
}
}
diff --git a/plugins/contact-assets/lang/es.json b/plugins/contact-assets/lang/es.json
index e7b28af657..833abbf4a1 100644
--- a/plugins/contact-assets/lang/es.json
+++ b/plugins/contact-assets/lang/es.json
@@ -119,6 +119,7 @@
"Everyone": "Todos",
"Here": "Aquí",
"EveryoneDescription": "Notificar a todos en este {title}",
- "HereDescription": "Notificar a todos en este {title}"
+ "HereDescription": "Notificar a todos en este {title}",
+ "Guest": "Invitado"
}
}
diff --git a/plugins/contact-assets/lang/fr.json b/plugins/contact-assets/lang/fr.json
index 60f5bd2af9..7073e20815 100644
--- a/plugins/contact-assets/lang/fr.json
+++ b/plugins/contact-assets/lang/fr.json
@@ -119,6 +119,7 @@
"Everyone": "Tout le monde",
"Here": "Ici",
"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é"
}
}
diff --git a/plugins/contact-assets/lang/it.json b/plugins/contact-assets/lang/it.json
index dfa097e478..edc3ddc2ac 100644
--- a/plugins/contact-assets/lang/it.json
+++ b/plugins/contact-assets/lang/it.json
@@ -119,6 +119,7 @@
"Everyone": "Tutti",
"Here": "Qui",
"EveryoneDescription": "Notifica tutti in questo {title}",
- "HereDescription": "Notifica tutti in questo {title}"
+ "HereDescription": "Notifica tutti in questo {title}",
+ "Guest": "Ospite"
}
}
diff --git a/plugins/contact-assets/lang/ja.json b/plugins/contact-assets/lang/ja.json
index d35d4c809b..334bd6d1de 100644
--- a/plugins/contact-assets/lang/ja.json
+++ b/plugins/contact-assets/lang/ja.json
@@ -119,6 +119,7 @@
"Everyone": "全員",
"Here": "ここ",
"EveryoneDescription": "この{title}のすべてに通知する",
- "HereDescription": "この{title}のすべてに通知する"
+ "HereDescription": "この{title}のすべてに通知する",
+ "Guest": "ゲスト"
}
}
\ No newline at end of file
diff --git a/plugins/contact-assets/lang/pt.json b/plugins/contact-assets/lang/pt.json
index 056c9a042e..079ec60abc 100644
--- a/plugins/contact-assets/lang/pt.json
+++ b/plugins/contact-assets/lang/pt.json
@@ -119,6 +119,7 @@
"Everyone": "Todos",
"Here": "Aqui",
"EveryoneDescription": "Notificar todos neste {title}",
- "HereDescription": "Notificar todos neste {title}"
+ "HereDescription": "Notificar todos neste {title}",
+ "Guest": "Convidado"
}
}
diff --git a/plugins/contact-assets/lang/ru.json b/plugins/contact-assets/lang/ru.json
index 9a5501ab2e..efd2ee0306 100644
--- a/plugins/contact-assets/lang/ru.json
+++ b/plugins/contact-assets/lang/ru.json
@@ -119,6 +119,7 @@
"Everyone": "Все",
"Here": "Здесь",
"EveryoneDescription": "Уведомить всех в этом {title}",
- "HereDescription": "Уведомить всех в этом{title}"
+ "HereDescription": "Уведомить всех в этом{title}",
+ "Guest": "Гость"
}
}
diff --git a/plugins/contact-assets/lang/zh.json b/plugins/contact-assets/lang/zh.json
index ed9149683c..848ebf2096 100644
--- a/plugins/contact-assets/lang/zh.json
+++ b/plugins/contact-assets/lang/zh.json
@@ -119,6 +119,7 @@
"Everyone": "所有人",
"Here": "这里",
"EveryoneDescription": "通知所有人在此{title}",
- "HereDescription": "通知所有人在此{title}"
+ "HereDescription": "通知所有人在此{title}",
+ "Guest": "访客"
}
}
diff --git a/plugins/contact-resources/src/components/Avatar.svelte b/plugins/contact-resources/src/components/Avatar.svelte
index a2969e67ec..1578aff712 100644
--- a/plugins/contact-resources/src/components/Avatar.svelte
+++ b/plugins/contact-resources/src/components/Avatar.svelte
@@ -123,7 +123,7 @@
-
+
{#if showStatus && person}
-
-
diff --git a/plugins/contact-resources/src/components/AvatarInstance.svelte b/plugins/contact-resources/src/components/AvatarInstance.svelte
index d0ca2f32b8..96e16263a0 100644
--- a/plugins/contact-resources/src/components/AvatarInstance.svelte
+++ b/plugins/contact-resources/src/components/AvatarInstance.svelte
@@ -34,7 +34,6 @@
export let adaptiveName: boolean = false
export let disabled: boolean = false
export let style: 'modern' | undefined = undefined
- export let clickable: boolean = false
function handleClick (): void {
dispatch('click')
@@ -73,7 +72,7 @@
{/if}
-
-
diff --git a/plugins/contact/src/index.ts b/plugins/contact/src/index.ts
index 9e49652b14..feaeabcc8e 100644
--- a/plugins/contact/src/index.ts
+++ b/plugins/contact/src/index.ts
@@ -346,7 +346,8 @@ export const contactPlugin = plugin(contactId, {
Everyone: '' as IntlString,
Here: '' as IntlString,
EveryoneDescription: '' as IntlString,
- HereDescription: '' as IntlString
+ HereDescription: '' as IntlString,
+ Guest: '' as IntlString
},
viewlet: {
TableMember: '' as Ref
,
diff --git a/plugins/login-resources/src/utils.ts b/plugins/login-resources/src/utils.ts
index e2a99ef602..4f8bb9d858 100644
--- a/plugins/login-resources/src/utils.ts
+++ b/plugins/login-resources/src/utils.ts
@@ -392,7 +392,7 @@ export async function fetchWorkspace (): Promise<[Status, WorkspaceInfoWithStatu
}
try {
- const workspaceWithStatus = await getAccountClient(token).getWorkspaceInfo()
+ const workspaceWithStatus = await getAccountClient(token).getWorkspaceInfo(true)
Analytics.handleEvent('Fetch workspace')
Analytics.setWorkspace(workspaceWithStatus.url)
diff --git a/services/mail/mail-common/src/__tests__/channel.test.ts b/services/mail/mail-common/src/__tests__/channel.test.ts
index 87736295a4..b35d202200 100644
--- a/services/mail/mail-common/src/__tests__/channel.test.ts
+++ b/services/mail/mail-common/src/__tests__/channel.test.ts
@@ -154,6 +154,82 @@ describe('ChannelCache', () => {
// Verify the cache doesn't contain the failed lookup
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', () => {
diff --git a/services/mail/mail-common/src/channel.ts b/services/mail/mail-common/src/channel.ts
index 3a024308e6..b0405c46b5 100644
--- a/services/mail/mail-common/src/channel.ts
+++ b/services/mail/mail-common/src/channel.ts
@@ -25,7 +25,7 @@ const createMutex = new SyncMutex()
* Caches channel references to reduce calls to create mail channels
*/
export class ChannelCache {
- // Key is `${spaceId}:${emailAccount}`
+ // Key is `${spaceId}:${normalizedEmail}`
private readonly cache = new Map>()
constructor (
@@ -40,17 +40,18 @@ export class ChannelCache {
async getOrCreateChannel (
spaceId: Ref,
participants: PersonId[],
- emailAccount: string,
+ email: string,
owner: PersonId
): Promise[ | undefined> {
- const cacheKey = `${spaceId}:${emailAccount}`
+ const normalizedEmail = normalizeEmail(email)
+ const cacheKey = `${spaceId}:${normalizedEmail}`
let channel = this.cache.get(cacheKey)
if (channel != null) {
return channel
}
- channel = await this.fetchOrCreateChannel(spaceId, participants, emailAccount, owner)
+ channel = await this.fetchOrCreateChannel(spaceId, participants, normalizedEmail, owner)
if (channel != null) {
this.cache.set(cacheKey, channel)
}
@@ -58,8 +59,9 @@ export class ChannelCache {
return channel
}
- clearCache (spaceId: Ref, emailAccount: string): void {
- this.cache.delete(`${spaceId}:${emailAccount}`)
+ clearCache (spaceId: Ref, email: string): void {
+ const normalizedEmail = normalizeEmail(email)
+ this.cache.delete(`${spaceId}:${normalizedEmail}`)
}
clearAllCache (): void {
@@ -73,29 +75,30 @@ export class ChannelCache {
private async fetchOrCreateChannel (
space: Ref,
participants: PersonId[],
- emailAccount: string,
+ email: string,
personId: PersonId
): Promise][ | undefined> {
+ const normalizedEmail = normalizeEmail(email)
try {
// 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) {
- 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 await this.createNewChannel(space, participants, emailAccount, personId)
+ return await this.createNewChannel(space, participants, normalizedEmail, personId)
} catch (err) {
this.ctx.error('Failed to create channel', {
- me: emailAccount,
+ me: normalizedEmail,
space,
workspace: this.workspace,
error: err instanceof Error ? err.message : String(err)
})
// Remove failed lookup from cache
- this.cache.delete(`${space}:${emailAccount}`)
+ this.cache.delete(`${space}:${normalizedEmail}`)
return undefined
}
@@ -104,18 +107,19 @@ export class ChannelCache {
private async createNewChannel (
space: Ref,
participants: PersonId[],
- emailAccount: string,
+ email: string,
personId: PersonId
): Promise][ | 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)
try {
// 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) {
this.ctx.info('Using existing channel (found after mutex lock)', {
- me: emailAccount,
+ me: normalizedEmail,
space,
channel: existingChannel._id
})
@@ -123,12 +127,12 @@ export class ChannelCache {
}
// 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(
chat.masterTag.Channel,
space,
{
- title: emailAccount,
+ title: normalizedEmail,
private: true,
members: participants,
archived: false,
@@ -140,7 +144,7 @@ export class ChannelCache {
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(
channelId,
chat.masterTag.Channel,
@@ -187,3 +191,7 @@ export const ChannelCacheFactory = {
return ChannelCacheFactory.instances.size
}
}
+
+function normalizeEmail (email: string): string {
+ return email.toLowerCase().trim()
+}
]