diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 01f83432fb5..9fa0051bfd2 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -18563,6 +18563,9 @@ importers: '@hcengineering/core': specifier: workspace:^0.7.26 version: link:../../foundations/core/packages/core + '@hcengineering/emoji-resources': + specifier: workspace:^0.7.0 + version: link:../emoji-resources '@hcengineering/image-cropper': specifier: workspace:^0.7.0 version: link:../image-cropper diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index a0291182995..d590f7012c8 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -34,7 +34,8 @@ import { type SocialIdentity, type Status, type SocialIdentityProvider, - type Translation + type Translation, + type WorkspaceMemberStatus } from '@hcengineering/contact' import { AccountRole, @@ -52,6 +53,7 @@ import { type PersonId, type PersonUuid, type Ref, + type Space, type Timestamp } from '@hcengineering/core' import { createSystemType } from '@hcengineering/model-card' @@ -72,6 +74,7 @@ import { TypeRef, TypeString, TypeTimestamp, + TypeAccountUuid, UX, type Builder } from '@hcengineering/model' @@ -301,6 +304,24 @@ export class TTranslation extends TPreference implements Translation { dontTranslate!: string[] } +@Model(contact.class.WorkspaceMemberStatus, core.class.Doc, DOMAIN_CONTACT) +@UX(contact.string.WorkspaceStatusNote, contact.icon.User) +export class TWorkspaceMemberStatus extends TDoc implements WorkspaceMemberStatus { + @Prop(TypeRef(core.class.Space), core.string.Space) + @Index(IndexKind.Indexed) + declare space: Ref + + @Prop(TypeAccountUuid(), core.string.Members) + @Index(IndexKind.Indexed) + user!: AccountUuid + + @Prop(TypeString(), core.string.Description) + message!: string + + @Prop(TypeTimestamp(), contact.string.StatusDueDate) + clearAt?: Timestamp +} + export function createModel (builder: Builder): void { builder.createModel( TAvatarProvider, @@ -317,13 +338,20 @@ export function createModel (builder: Builder): void { TContactsTab, TPersonSpace, TUserRole, - TTranslation + TTranslation, + TWorkspaceMemberStatus ) builder.mixin(contact.class.PersonSpace, core.class.Class, core.mixin.TxAccessLevel, { createAccessLevel: AccountRole.Guest }) + builder.mixin(contact.class.WorkspaceMemberStatus, core.class.Class, core.mixin.TxAccessLevel, { + createAccessLevel: AccountRole.Guest, + updateAccessLevel: AccountRole.Guest, + removeAccessLevel: AccountRole.Guest + }) + builder.mixin(contact.class.Person, core.class.Class, core.mixin.TxAccessLevel, { createAccessLevel: AccountRole.Guest, isIdentity: true @@ -1401,6 +1429,12 @@ export function createModel (builder: Builder): void { _class: 1, [contact.mixin.Employee + '.active']: 1 } + }, + { + keys: { + _class: 1, + user: 1 + } } ], disabled: [{ attachedToClass: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }, { attachedTo: 1 }] diff --git a/plugins/contact-assets/lang/cs.json b/plugins/contact-assets/lang/cs.json index b3a47e6c420..9c821d94803 100644 --- a/plugins/contact-assets/lang/cs.json +++ b/plugins/contact-assets/lang/cs.json @@ -111,6 +111,10 @@ "ViewProfile": "Zobrazit profil", "Viber": "Viber", "ViberPlaceholder": "Viber", + "SocialId": "Sociální ID", + "SocialIds": "Sociální ID", + "Type": "Typ", + "Confirmed": "Potvrzeno", "UserProfile": "Uživatelský profil", "DeactivatedAccount": "Deaktivovaný účet", "LocalTime": "místní čas", @@ -125,6 +129,26 @@ "TranslateTo": "Přeložit do", "DontTranslate": "Nepřekládat", "SelectLanguage": "Vyberte jazyk", - "SelectLanguages": "Vyberte jazyky" + "SelectLanguages": "Vyberte jazyky", + "WorkspaceStatusNote": "Stav uživatele", + "WorkspaceStatusUntil": "Zobrazit do", + "WorkspaceStatusMenu": "Váš stav", + "WorkspaceStatusSetYour": "Nastavte svůj stav", + "WorkspaceStatusUpdateYour": "Aktualizujte svůj stav", + "WorkspaceStatusMessage": "Jaký je váš stav?", + "WorkspaceStatusAway": "Pryč", + "WorkspaceStatusVacation": "Na dovolené", + "WorkspaceStatusOutSick": "Nemoc", + "WorkspaceStatusClear": "Vymazat stav", + "WorkspaceStatusSave": "Uložit", + "WorkspaceStatusQuickPick": "Rychlé stavy", + "WorkspaceStatusComposeHint": "Krátká poznámka (volitelné)", + "WorkspaceStatusClearUntil": "Vymazat datum", + "WorkspaceStatusDoNotClear": "Stav automaticky nečistit", + "WorkspaceStatusIn30Min": "Za 30 minut", + "WorkspaceStatusIn1Hour": "Za 1 hodinu", + "WorkspaceStatusIn4Hours": "Za 4 hodiny", + "WorkspaceStatusEndOfDay": "Do konce dne", + "WorkspaceStatusPickDate": "Vyberte datum" } } diff --git a/plugins/contact-assets/lang/de.json b/plugins/contact-assets/lang/de.json index 9d58954a88f..eaf64bfafe7 100644 --- a/plugins/contact-assets/lang/de.json +++ b/plugins/contact-assets/lang/de.json @@ -111,6 +111,10 @@ "ViewProfile": "Profil anzeigen", "Viber": "Viber", "ViberPlaceholder": "Viber", + "SocialId": "Social-ID", + "SocialIds": "Social-IDs", + "Type": "Typ", + "Confirmed": "Bestätigt", "UserProfile": "Benutzerprofil", "DeactivatedAccount": "Deaktivierter Account", "LocalTime": "Ortszeit", @@ -125,6 +129,26 @@ "TranslateTo": "Übersetzen in", "DontTranslate": "Nicht übersetzen", "SelectLanguage": "Sprache auswählen", - "SelectLanguages": "Sprachen auswählen" + "SelectLanguages": "Sprachen auswählen", + "WorkspaceStatusNote": "Benutzerstatus", + "WorkspaceStatusUntil": "Anzeigen bis", + "WorkspaceStatusMenu": "Ihr Status", + "WorkspaceStatusSetYour": "Status festlegen", + "WorkspaceStatusUpdateYour": "Status aktualisieren", + "WorkspaceStatusMessage": "Wie lautet Ihr Status?", + "WorkspaceStatusAway": "Abwesend", + "WorkspaceStatusVacation": "Im Urlaub", + "WorkspaceStatusOutSick": "Krank", + "WorkspaceStatusClear": "Status löschen", + "WorkspaceStatusSave": "Speichern", + "WorkspaceStatusQuickPick": "Schnellauswahl", + "WorkspaceStatusComposeHint": "Kurze Notiz hinzufügen (optional)", + "WorkspaceStatusClearUntil": "Datum löschen", + "WorkspaceStatusDoNotClear": "Status nicht automatisch löschen", + "WorkspaceStatusIn30Min": "In 30 Minuten", + "WorkspaceStatusIn1Hour": "In 1 Stunde", + "WorkspaceStatusIn4Hours": "In 4 Stunden", + "WorkspaceStatusEndOfDay": "Ende des Tages", + "WorkspaceStatusPickDate": "Datum wählen" } } diff --git a/plugins/contact-assets/lang/en.json b/plugins/contact-assets/lang/en.json index a3fef6df7c7..315baf7a164 100644 --- a/plugins/contact-assets/lang/en.json +++ b/plugins/contact-assets/lang/en.json @@ -129,6 +129,26 @@ "TranslateTo": "Translate to", "DontTranslate": "Don't translate", "SelectLanguage": "Select language", - "SelectLanguages": "Select languages" + "SelectLanguages": "Select languages", + "WorkspaceStatusNote": "User status", + "WorkspaceStatusUntil": "Show until", + "WorkspaceStatusMenu": "Your status", + "WorkspaceStatusSetYour": "Set your status", + "WorkspaceStatusUpdateYour": "Update your status", + "WorkspaceStatusMessage": "What is your status?", + "WorkspaceStatusAway": "Away", + "WorkspaceStatusVacation": "On vacation", + "WorkspaceStatusOutSick": "Out sick", + "WorkspaceStatusClear": "Clear status", + "WorkspaceStatusSave": "Save", + "WorkspaceStatusQuickPick": "Quick statuses", + "WorkspaceStatusComposeHint": "Add a short note (optional)", + "WorkspaceStatusClearUntil": "Clear date", + "WorkspaceStatusDoNotClear": "Do not clear status", + "WorkspaceStatusIn30Min": "In 30 minutes", + "WorkspaceStatusIn1Hour": "In 1 hour", + "WorkspaceStatusIn4Hours": "In 4 hours", + "WorkspaceStatusEndOfDay": "End of day", + "WorkspaceStatusPickDate": "Choose date" } } diff --git a/plugins/contact-assets/lang/es.json b/plugins/contact-assets/lang/es.json index c1246161db9..a1a68375677 100644 --- a/plugins/contact-assets/lang/es.json +++ b/plugins/contact-assets/lang/es.json @@ -129,6 +129,26 @@ "TranslateTo": "Traducir a", "DontTranslate": "No traducir", "SelectLanguage": "Seleccionar idioma", - "SelectLanguages": "Seleccionar idiomas" + "SelectLanguages": "Seleccionar idiomas", + "WorkspaceStatusNote": "Estado del usuario", + "WorkspaceStatusUntil": "Mostrar hasta", + "WorkspaceStatusMenu": "Tu estado", + "WorkspaceStatusSetYour": "Establecer tu estado", + "WorkspaceStatusUpdateYour": "Actualizar tu estado", + "WorkspaceStatusMessage": "¿Cuál es tu estado?", + "WorkspaceStatusAway": "Ausente", + "WorkspaceStatusVacation": "De vacaciones", + "WorkspaceStatusOutSick": "De baja por enfermedad", + "WorkspaceStatusClear": "Borrar estado", + "WorkspaceStatusSave": "Guardar", + "WorkspaceStatusQuickPick": "Estados rápidos", + "WorkspaceStatusComposeHint": "Añade una nota breve (opcional)", + "WorkspaceStatusClearUntil": "Borrar fecha", + "WorkspaceStatusDoNotClear": "No borrar el estado automáticamente", + "WorkspaceStatusIn30Min": "En 30 minutos", + "WorkspaceStatusIn1Hour": "En 1 hora", + "WorkspaceStatusIn4Hours": "En 4 horas", + "WorkspaceStatusEndOfDay": "Fin del día", + "WorkspaceStatusPickDate": "Elegir fecha" } } diff --git a/plugins/contact-assets/lang/fr.json b/plugins/contact-assets/lang/fr.json index 8e17e7b97f6..8401064a71c 100644 --- a/plugins/contact-assets/lang/fr.json +++ b/plugins/contact-assets/lang/fr.json @@ -129,6 +129,26 @@ "TranslateTo": "Traduire en", "DontTranslate": "Ne pas traduire", "SelectLanguage": "Sélectionner la langue", - "SelectLanguages": "Sélectionner les langues" + "SelectLanguages": "Sélectionner les langues", + "WorkspaceStatusNote": "Statut utilisateur", + "WorkspaceStatusUntil": "Afficher jusqu’au", + "WorkspaceStatusMenu": "Votre statut", + "WorkspaceStatusSetYour": "Définir votre statut", + "WorkspaceStatusUpdateYour": "Mettre à jour votre statut", + "WorkspaceStatusMessage": "Quel est votre statut ?", + "WorkspaceStatusAway": "Absent", + "WorkspaceStatusVacation": "En congé", + "WorkspaceStatusOutSick": "Malade", + "WorkspaceStatusClear": "Effacer le statut", + "WorkspaceStatusSave": "Enregistrer", + "WorkspaceStatusQuickPick": "Statuts rapides", + "WorkspaceStatusComposeHint": "Ajouter une courte note (facultatif)", + "WorkspaceStatusClearUntil": "Effacer la date", + "WorkspaceStatusDoNotClear": "Ne pas effacer le statut automatiquement", + "WorkspaceStatusIn30Min": "Dans 30 minutes", + "WorkspaceStatusIn1Hour": "Dans 1 heure", + "WorkspaceStatusIn4Hours": "Dans 4 heures", + "WorkspaceStatusEndOfDay": "Fin de journée", + "WorkspaceStatusPickDate": "Choisir une date" } } diff --git a/plugins/contact-assets/lang/it.json b/plugins/contact-assets/lang/it.json index 0a688aea02f..3f853c6e306 100644 --- a/plugins/contact-assets/lang/it.json +++ b/plugins/contact-assets/lang/it.json @@ -129,6 +129,26 @@ "TranslateTo": "Traduci in", "DontTranslate": "Non tradurre", "SelectLanguage": "Seleziona lingua", - "SelectLanguages": "Seleziona lingue" + "SelectLanguages": "Seleziona lingue", + "WorkspaceStatusNote": "Stato utente", + "WorkspaceStatusUntil": "Mostra fino al", + "WorkspaceStatusMenu": "Il tuo stato", + "WorkspaceStatusSetYour": "Imposta il tuo stato", + "WorkspaceStatusUpdateYour": "Aggiorna il tuo stato", + "WorkspaceStatusMessage": "Qual è il tuo stato?", + "WorkspaceStatusAway": "Assente", + "WorkspaceStatusVacation": "In ferie", + "WorkspaceStatusOutSick": "Malato", + "WorkspaceStatusClear": "Cancella stato", + "WorkspaceStatusSave": "Salva", + "WorkspaceStatusQuickPick": "Stati rapidi", + "WorkspaceStatusComposeHint": "Aggiungi una breve nota (facoltativo)", + "WorkspaceStatusClearUntil": "Cancella data", + "WorkspaceStatusDoNotClear": "Non cancellare lo stato automaticamente", + "WorkspaceStatusIn30Min": "Tra 30 minuti", + "WorkspaceStatusIn1Hour": "Tra 1 ora", + "WorkspaceStatusIn4Hours": "Tra 4 ore", + "WorkspaceStatusEndOfDay": "Fine giornata", + "WorkspaceStatusPickDate": "Scegli data" } } diff --git a/plugins/contact-assets/lang/ja.json b/plugins/contact-assets/lang/ja.json index 27f8725792c..82575cdba0f 100644 --- a/plugins/contact-assets/lang/ja.json +++ b/plugins/contact-assets/lang/ja.json @@ -129,6 +129,26 @@ "TranslateTo": "翻訳先", "DontTranslate": "翻訳しない", "SelectLanguage": "言語を選択", - "SelectLanguages": "言語を選択" + "SelectLanguages": "言語を選択", + "WorkspaceStatusNote": "ユーザーステータス", + "WorkspaceStatusUntil": "表示期限", + "WorkspaceStatusMenu": "あなたのステータス", + "WorkspaceStatusSetYour": "ステータスを設定", + "WorkspaceStatusUpdateYour": "ステータスを更新", + "WorkspaceStatusMessage": "ステータスは何ですか?", + "WorkspaceStatusAway": "離席中", + "WorkspaceStatusVacation": "休暇中", + "WorkspaceStatusOutSick": "病欠", + "WorkspaceStatusClear": "ステータスをクリア", + "WorkspaceStatusSave": "保存", + "WorkspaceStatusQuickPick": "クイックステータス", + "WorkspaceStatusComposeHint": "短いメモを追加(任意)", + "WorkspaceStatusClearUntil": "日付をクリア", + "WorkspaceStatusDoNotClear": "ステータスを自動ではクリアしない", + "WorkspaceStatusIn30Min": "30分後", + "WorkspaceStatusIn1Hour": "1時間後", + "WorkspaceStatusIn4Hours": "4時間後", + "WorkspaceStatusEndOfDay": "今日の終わりまで", + "WorkspaceStatusPickDate": "日付を選択" } } diff --git a/plugins/contact-assets/lang/pt-br.json b/plugins/contact-assets/lang/pt-br.json index 5500dac95a9..f9624fb6c57 100644 --- a/plugins/contact-assets/lang/pt-br.json +++ b/plugins/contact-assets/lang/pt-br.json @@ -129,6 +129,26 @@ "TranslateTo": "Traduzir para", "DontTranslate": "Não traduzir", "SelectLanguage": "Selecionar idioma", - "SelectLanguages": "Selecionar idiomas" + "SelectLanguages": "Selecionar idiomas", + "WorkspaceStatusNote": "Status do usuário", + "WorkspaceStatusUntil": "Mostrar até", + "WorkspaceStatusMenu": "Seu status", + "WorkspaceStatusSetYour": "Definir seu status", + "WorkspaceStatusUpdateYour": "Atualizar seu status", + "WorkspaceStatusMessage": "Qual é o seu status?", + "WorkspaceStatusAway": "Ausente", + "WorkspaceStatusVacation": "De férias", + "WorkspaceStatusOutSick": "Afastado por saúde", + "WorkspaceStatusClear": "Limpar status", + "WorkspaceStatusSave": "Salvar", + "WorkspaceStatusQuickPick": "Status rápidos", + "WorkspaceStatusComposeHint": "Adicione uma nota curta (opcional)", + "WorkspaceStatusClearUntil": "Limpar data", + "WorkspaceStatusDoNotClear": "Não limpar o status automaticamente", + "WorkspaceStatusIn30Min": "Em 30 minutos", + "WorkspaceStatusIn1Hour": "Em 1 hora", + "WorkspaceStatusIn4Hours": "Em 4 horas", + "WorkspaceStatusEndOfDay": "Fim do dia", + "WorkspaceStatusPickDate": "Escolher data" } } diff --git a/plugins/contact-assets/lang/pt.json b/plugins/contact-assets/lang/pt.json index d2e55eb879a..759d6f10c25 100644 --- a/plugins/contact-assets/lang/pt.json +++ b/plugins/contact-assets/lang/pt.json @@ -129,6 +129,26 @@ "TranslateTo": "Traduzir para", "DontTranslate": "Não traduzir", "SelectLanguage": "Selecionar idioma", - "SelectLanguages": "Selecionar idiomas" + "SelectLanguages": "Selecionar idiomas", + "WorkspaceStatusNote": "Estado do utilizador", + "WorkspaceStatusUntil": "Mostrar até", + "WorkspaceStatusMenu": "O seu estado", + "WorkspaceStatusSetYour": "Definir o seu estado", + "WorkspaceStatusUpdateYour": "Atualizar o seu estado", + "WorkspaceStatusMessage": "Qual é o seu estado?", + "WorkspaceStatusAway": "Ausente", + "WorkspaceStatusVacation": "De férias", + "WorkspaceStatusOutSick": "Baixa por doença", + "WorkspaceStatusClear": "Limpar estado", + "WorkspaceStatusSave": "Guardar", + "WorkspaceStatusQuickPick": "Estados rápidos", + "WorkspaceStatusComposeHint": "Adicionar uma nota breve (opcional)", + "WorkspaceStatusClearUntil": "Limpar data", + "WorkspaceStatusDoNotClear": "Não limpar o estado automaticamente", + "WorkspaceStatusIn30Min": "Em 30 minutos", + "WorkspaceStatusIn1Hour": "Em 1 hora", + "WorkspaceStatusIn4Hours": "Em 4 horas", + "WorkspaceStatusEndOfDay": "Fim do dia", + "WorkspaceStatusPickDate": "Escolher data" } } diff --git a/plugins/contact-assets/lang/ru.json b/plugins/contact-assets/lang/ru.json index cec1b52da16..e98bbc58929 100644 --- a/plugins/contact-assets/lang/ru.json +++ b/plugins/contact-assets/lang/ru.json @@ -129,6 +129,26 @@ "TranslateTo": "Переводить на", "DontTranslate": "Не переводить", "SelectLanguage": "Выберите язык", - "SelectLanguages": "Выберите языки" + "SelectLanguages": "Выберите языки", + "WorkspaceStatusNote": "Статус пользователя", + "WorkspaceStatusUntil": "Показывать до", + "WorkspaceStatusMenu": "Ваш статус", + "WorkspaceStatusSetYour": "Установить статус", + "WorkspaceStatusUpdateYour": "Обновить статус", + "WorkspaceStatusMessage": "Какой ваш статус?", + "WorkspaceStatusAway": "Отошёл", + "WorkspaceStatusVacation": "В отпуске", + "WorkspaceStatusOutSick": "На больничном", + "WorkspaceStatusClear": "Очистить статус", + "WorkspaceStatusSave": "Сохранить", + "WorkspaceStatusQuickPick": "Быстрые статусы", + "WorkspaceStatusComposeHint": "Краткая заметка (необязательно)", + "WorkspaceStatusClearUntil": "Сбросить дату", + "WorkspaceStatusDoNotClear": "Не сбрасывать статус автоматически", + "WorkspaceStatusIn30Min": "Через 30 минут", + "WorkspaceStatusIn1Hour": "Через 1 час", + "WorkspaceStatusIn4Hours": "Через 4 часа", + "WorkspaceStatusEndOfDay": "До конца дня", + "WorkspaceStatusPickDate": "Выбрать дату" } } diff --git a/plugins/contact-assets/lang/tr.json b/plugins/contact-assets/lang/tr.json index 424a04d6b51..efbea87cea5 100644 --- a/plugins/contact-assets/lang/tr.json +++ b/plugins/contact-assets/lang/tr.json @@ -124,6 +124,31 @@ "HereDescription": "Bu {title} içindeki çevrimiçi üyeleri bilgilendir", "Guest": "Misafir", "Deleted": "Silindi", - "CannotMerge": "Hata: Global kişiler birleştirilemez" + "CannotMerge": "Hata: Global kişiler birleştirilemez", + "AutoTranslation": "Otomatik çeviri", + "TranslateTo": "Şuna çevir", + "DontTranslate": "Çevirme", + "SelectLanguage": "Dil seç", + "SelectLanguages": "Dilleri seç", + "WorkspaceStatusNote": "Kullanıcı durumu", + "WorkspaceStatusUntil": "Şu tarihe kadar göster", + "WorkspaceStatusMenu": "Durumunuz", + "WorkspaceStatusSetYour": "Durumunuzu ayarlayın", + "WorkspaceStatusUpdateYour": "Durumunuzu güncelleyin", + "WorkspaceStatusMessage": "Durumunuz nedir?", + "WorkspaceStatusAway": "Uzakta", + "WorkspaceStatusVacation": "Tatilde", + "WorkspaceStatusOutSick": "Hastalık izni", + "WorkspaceStatusClear": "Durumu temizle", + "WorkspaceStatusSave": "Kaydet", + "WorkspaceStatusQuickPick": "Hızlı durumlar", + "WorkspaceStatusComposeHint": "Kısa bir not ekleyin (isteğe bağlı)", + "WorkspaceStatusClearUntil": "Tarihi temizle", + "WorkspaceStatusDoNotClear": "Durumu otomatik temizleme", + "WorkspaceStatusIn30Min": "30 dakika içinde", + "WorkspaceStatusIn1Hour": "1 saat içinde", + "WorkspaceStatusIn4Hours": "4 saat içinde", + "WorkspaceStatusEndOfDay": "Gün sonu", + "WorkspaceStatusPickDate": "Tarih seç" } } diff --git a/plugins/contact-assets/lang/zh.json b/plugins/contact-assets/lang/zh.json index 12132b87f05..663081fbcd2 100644 --- a/plugins/contact-assets/lang/zh.json +++ b/plugins/contact-assets/lang/zh.json @@ -129,6 +129,26 @@ "TranslateTo": "翻译为", "DontTranslate": "不翻译", "SelectLanguage": "选择语言", - "SelectLanguages": "选择语言" + "SelectLanguages": "选择语言", + "WorkspaceStatusNote": "用户状态", + "WorkspaceStatusUntil": "显示截止日期", + "WorkspaceStatusMenu": "你的状态", + "WorkspaceStatusSetYour": "设置你的状态", + "WorkspaceStatusUpdateYour": "更新你的状态", + "WorkspaceStatusMessage": "你的状态是什么?", + "WorkspaceStatusAway": "离开", + "WorkspaceStatusVacation": "休假中", + "WorkspaceStatusOutSick": "病假", + "WorkspaceStatusClear": "清除状态", + "WorkspaceStatusSave": "保存", + "WorkspaceStatusQuickPick": "快捷状态", + "WorkspaceStatusComposeHint": "添加简短备注(可选)", + "WorkspaceStatusClearUntil": "清除日期", + "WorkspaceStatusDoNotClear": "不自动清除状态", + "WorkspaceStatusIn30Min": "30 分钟后", + "WorkspaceStatusIn1Hour": "1 小时后", + "WorkspaceStatusIn4Hours": "4 小时后", + "WorkspaceStatusEndOfDay": "今天结束", + "WorkspaceStatusPickDate": "选择日期" } } diff --git a/plugins/contact-resources/package.json b/plugins/contact-resources/package.json index a90f68c3f63..898119415a8 100644 --- a/plugins/contact-resources/package.json +++ b/plugins/contact-resources/package.json @@ -45,6 +45,7 @@ "@hcengineering/attachment-resources": "workspace:^0.7.0", "@hcengineering/contact": "workspace:^0.7.0", "@hcengineering/core": "workspace:^0.7.26", + "@hcengineering/emoji-resources": "workspace:^0.7.0", "@hcengineering/image-cropper": "workspace:^0.7.0", "@hcengineering/login": "workspace:^0.7.0", "@hcengineering/notification": "workspace:^0.7.0", diff --git a/plugins/contact-resources/src/components/EmployeePresenter.svelte b/plugins/contact-resources/src/components/EmployeePresenter.svelte index b3d5f10c361..701eb30e8f0 100644 --- a/plugins/contact-resources/src/components/EmployeePresenter.svelte +++ b/plugins/contact-resources/src/components/EmployeePresenter.svelte @@ -1,10 +1,17 @@ - +
+ + {#if showWorkspaceStatusEmoji && shouldShowName && statusEmoji !== undefined && isWorkspaceMemberStatusVisible(statusDoc)} + + {statusEmoji} + + {/if} +
+ + diff --git a/plugins/contact-resources/src/components/WorkspaceMemberStatusEditor.svelte b/plugins/contact-resources/src/components/WorkspaceMemberStatusEditor.svelte new file mode 100644 index 00000000000..538e10fda62 --- /dev/null +++ b/plugins/contact-resources/src/components/WorkspaceMemberStatusEditor.svelte @@ -0,0 +1,545 @@ + + + +
+
+
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+ {#each presets as item} + + {/each} +
+ +
+
+
+ +
+ {#if clearAt !== undefined && clearMode === 'pickDate'} +
+
+ +
+ {#key clearAt} + + {/key} +
+ {/if} +
+ + +
+ + + diff --git a/plugins/contact-resources/src/components/person/EmployeePreviewPopup.svelte b/plugins/contact-resources/src/components/person/EmployeePreviewPopup.svelte index 3d510f157fe..c35eb6f5d67 100644 --- a/plugins/contact-resources/src/components/person/EmployeePreviewPopup.svelte +++ b/plugins/contact-resources/src/components/person/EmployeePreviewPopup.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> @@ -108,8 +114,21 @@ on:click={viewProfile} />
-
- + {#if statusSubtitle} +
+
+ {statusSubtitle} +
+
+ {/if} + {#if hasRating}
diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index 27a2116cf32..315b4547104 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -110,6 +110,7 @@ import SelectAvatars from './components/SelectAvatars.svelte' import SelectUsersPopup from './components/SelectUsersPopup.svelte' import SocialEditor from './components/SocialEditor.svelte' import SpaceMembers from './components/SpaceMembers.svelte' +import WorkspaceMemberStatusEditor from './components/WorkspaceMemberStatusEditor.svelte' import SpaceMembersEditor from './components/SpaceMembersEditor.svelte' import SystemAvatar from './components/SystemAvatar.svelte' import UserBox from './components/UserBox.svelte' @@ -155,6 +156,7 @@ import { } from './utils' export * from './utils' +export * from './workspaceMemberStatus' export { employeeByIdStore } from './utils' export * from './assignee' export * from './translation' @@ -199,6 +201,7 @@ export { SelectAvatars, SelectUsersPopup, SpaceMembers, + WorkspaceMemberStatusEditor, SystemAvatar, UserBox, UserBoxItems, @@ -403,7 +406,8 @@ export default async (): Promise => ({ PersonIdFilter, AssigneePopup, TranslationSettings, - SocialIdentityPresenter + SocialIdentityPresenter, + WorkspaceMemberStatusEditor }, completion: { EmployeeQuery: async ( diff --git a/plugins/contact-resources/src/plugin.ts b/plugins/contact-resources/src/plugin.ts index 5319090a133..f2f6bfc5e3a 100644 --- a/plugins/contact-resources/src/plugin.ts +++ b/plugins/contact-resources/src/plugin.ts @@ -90,7 +90,26 @@ export default mergeIds(contactId, contact, { TranslateTo: '' as IntlString, DontTranslate: '' as IntlString, SelectLanguage: '' as IntlString, - SelectLanguages: '' as IntlString + SelectLanguages: '' as IntlString, + WorkspaceStatusUntil: '' as IntlString, + WorkspaceStatusMenu: '' as IntlString, + WorkspaceStatusMessage: '' as IntlString, + WorkspaceStatusSetYour: '' as IntlString, + WorkspaceStatusUpdateYour: '' as IntlString, + WorkspaceStatusAway: '' as IntlString, + WorkspaceStatusVacation: '' as IntlString, + WorkspaceStatusOutSick: '' as IntlString, + WorkspaceStatusClear: '' as IntlString, + WorkspaceStatusSave: '' as IntlString, + WorkspaceStatusQuickPick: '' as IntlString, + WorkspaceStatusComposeHint: '' as IntlString, + WorkspaceStatusClearUntil: '' as IntlString, + WorkspaceStatusDoNotClear: '' as IntlString, + WorkspaceStatusIn30Min: '' as IntlString, + WorkspaceStatusIn1Hour: '' as IntlString, + WorkspaceStatusIn4Hours: '' as IntlString, + WorkspaceStatusEndOfDay: '' as IntlString, + WorkspaceStatusPickDate: '' as IntlString }, function: { GetContactLink: '' as Resource<(doc: Doc, props: Record) => Promise>, diff --git a/plugins/contact-resources/src/utils.ts b/plugins/contact-resources/src/utils.ts index 164757d75cd..cb0eac72320 100644 --- a/plugins/contact-resources/src/utils.ts +++ b/plugins/contact-resources/src/utils.ts @@ -14,6 +14,8 @@ // limitations under the License. // +import { loadWorkspaceMemberStatuses } from './workspaceMemberStatus' + import { type AccountClient, getClient as getAccountClientRaw } from '@hcengineering/account-client' import { addEmployeeListenrer, @@ -411,6 +413,8 @@ onClient(() => { channelProviders.set(res) }) + loadWorkspaceMemberStatuses() + employeesQuery.query( contact.mixin.Employee, {}, diff --git a/plugins/contact-resources/src/workspaceMemberStatus.ts b/plugins/contact-resources/src/workspaceMemberStatus.ts new file mode 100644 index 00000000000..cc075834e67 --- /dev/null +++ b/plugins/contact-resources/src/workspaceMemberStatus.ts @@ -0,0 +1,33 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import contact, { type WorkspaceMemberStatus } from '@hcengineering/contact' +import type { AccountUuid } from '@hcengineering/core' +import { writable } from 'svelte/store' + +import { createQuery } from '@hcengineering/presentation' + +export const workspaceMemberStatusByAccountStore = writable>(new Map()) + +const workspaceMemberStatusQuery = createQuery(true) +let workspaceStatusesLoaded = false + +export function loadWorkspaceMemberStatuses (): void { + if (workspaceStatusesLoaded) return + workspaceStatusesLoaded = true + workspaceMemberStatusQuery.query(contact.class.WorkspaceMemberStatus, {}, (res) => { + workspaceMemberStatusByAccountStore.set(new Map(res.map((it) => [it.user, it]))) + }) +} diff --git a/plugins/contact/src/__tests__/workspaceMemberStatusUtils.test.ts b/plugins/contact/src/__tests__/workspaceMemberStatusUtils.test.ts new file mode 100644 index 00000000000..84e8d34fbf7 --- /dev/null +++ b/plugins/contact/src/__tests__/workspaceMemberStatusUtils.test.ts @@ -0,0 +1,47 @@ +import { + WORKSPACE_MEMBER_STATUS_MESSAGE_MAX, + extractLeadingStatusEmoji, + getWorkspaceMemberStatusSubtitle, + isWorkspaceMemberStatusVisible, + trimWorkspaceMemberStatusMessage +} from '../workspaceMemberStatusUtils' + +describe('workspaceMemberStatusUtils', () => { + it('trimWorkspaceMemberStatusMessage enforces max length', () => { + const long = 'x'.repeat(WORKSPACE_MEMBER_STATUS_MESSAGE_MAX + 50) + expect(trimWorkspaceMemberStatusMessage(long).length).toBe(WORKSPACE_MEMBER_STATUS_MESSAGE_MAX) + expect(trimWorkspaceMemberStatusMessage(' hi ')).toBe('hi') + }) + + it('trimWorkspaceMemberStatusMessage keeps emoji intact at boundary', () => { + const max = WORKSPACE_MEMBER_STATUS_MESSAGE_MAX + const input = 'a'.repeat(max - 1) + '😀' + 'z' + const out = trimWorkspaceMemberStatusMessage(input) + expect(Array.from(out).length).toBe(max) + expect(out.endsWith('😀')).toBe(true) + }) + + it('isWorkspaceMemberStatusVisible respects clearAt and empty message', () => { + const now = 1_000_000 + expect(isWorkspaceMemberStatusVisible(undefined, now)).toBe(false) + expect(isWorkspaceMemberStatusVisible({ message: '', clearAt: now - 1 }, now)).toBe(false) + expect(isWorkspaceMemberStatusVisible({ message: '' }, now)).toBe(false) + expect(isWorkspaceMemberStatusVisible({ message: 'Hi' }, now)).toBe(true) + expect(isWorkspaceMemberStatusVisible({ message: '🏖️' }, now)).toBe(true) + }) + + it('getWorkspaceMemberStatusSubtitle returns trimmed message when visible', () => { + const now = 10_000_000 + expect(getWorkspaceMemberStatusSubtitle({ message: '🏖️ Beach', clearAt: now + 1000 }, now)).toBe('🏖️ Beach') + expect(getWorkspaceMemberStatusSubtitle({ message: 'Only text' }, now)).toBe('Only text') + expect(getWorkspaceMemberStatusSubtitle({ message: '' }, now)).toBe(undefined) + }) + + it('extractLeadingStatusEmoji takes first emoji sequence', () => { + expect(extractLeadingStatusEmoji('🏖️ Holiday')).toBe('🏖️') + expect(extractLeadingStatusEmoji('⏳ In a meeting')).toBe('⏳') + expect(extractLeadingStatusEmoji('No emoji')).toBe(undefined) + expect(extractLeadingStatusEmoji('')).toBe(undefined) + expect(extractLeadingStatusEmoji('👋')).toBe('👋') + }) +}) diff --git a/plugins/contact/src/index.ts b/plugins/contact/src/index.ts index ca103a8b81a..17deee76fa9 100644 --- a/plugins/contact/src/index.ts +++ b/plugins/contact/src/index.ts @@ -217,6 +217,17 @@ export interface Translation extends Preference { dontTranslate: string[] } +/** + * Persistent user status in workspace (e.g. vacation, short note). + * @public + */ +export interface WorkspaceMemberStatus extends Doc { + space: Ref + user: AccountUuid + message: string + clearAt?: Timestamp +} + /** * @public */ @@ -236,7 +247,8 @@ export const contactPlugin = plugin(contactId, { SocialIdentity: '' as Ref>, UserProfile: '' as Ref, UserRole: '' as Ref>, - Translation: '' as Ref> + Translation: '' as Ref>, + WorkspaceMemberStatus: '' as Ref> }, mixin: { Employee: '' as Ref> @@ -264,7 +276,8 @@ export const contactPlugin = plugin(contactId, { PersonFilterValuePresenter: '' as AnyComponent, PersonIdFilter: '' as AnyComponent, AssigneePopup: '' as AnyComponent, - EmployeePresenter: '' as AnyComponent + EmployeePresenter: '' as AnyComponent, + WorkspaceMemberStatusEditor: '' as AnyComponent }, channelProvider: { Email: '' as Ref, @@ -378,7 +391,8 @@ export const contactPlugin = plugin(contactId, { HereDescription: '' as IntlString, Guest: '' as IntlString, Deleted: '' as IntlString, - Email: '' as IntlString + Email: '' as IntlString, + WorkspaceStatusNote: '' as IntlString }, viewlet: { TableMember: '' as Ref, @@ -429,3 +443,10 @@ export * from './types' export * from './utils' export * from './analytics' export * from './avatar' +export { + WORKSPACE_MEMBER_STATUS_MESSAGE_MAX, + trimWorkspaceMemberStatusMessage, + isWorkspaceMemberStatusVisible, + getWorkspaceMemberStatusSubtitle, + extractLeadingStatusEmoji +} from './workspaceMemberStatusUtils' diff --git a/plugins/contact/src/workspaceMemberStatusUtils.ts b/plugins/contact/src/workspaceMemberStatusUtils.ts new file mode 100644 index 00000000000..5547093330b --- /dev/null +++ b/plugins/contact/src/workspaceMemberStatusUtils.ts @@ -0,0 +1,94 @@ +// +// Copyright © 2026 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. +// + +/** @public */ +export const WORKSPACE_MEMBER_STATUS_MESSAGE_MAX = 200 + +/** + * Minimal shape for status helpers (see {@link WorkspaceMemberStatus}). + */ +interface WorkspaceMemberStatusLike { + message: string + clearAt?: number +} + +/** @public */ +export function trimWorkspaceMemberStatusMessage (message: string): string { + const trimmed = message.trim() + return Array.from(trimmed).slice(0, WORKSPACE_MEMBER_STATUS_MESSAGE_MAX).join('') +} + +/** @public */ +export function isWorkspaceMemberStatusVisible ( + s: WorkspaceMemberStatusLike | undefined, + now: number = Date.now() +): boolean { + if (s === undefined) return false + if (s.clearAt !== undefined && s.clearAt <= now) return false + const m = s.message?.trim() ?? '' + return m !== '' +} + +/** + * Plain-text line for member lists / tooltips (full stored message, including any leading emoji). + * @public + */ +export function getWorkspaceMemberStatusSubtitle ( + s: WorkspaceMemberStatusLike | undefined, + now: number = Date.now() +): string | undefined { + if (!isWorkspaceMemberStatusVisible(s, now)) return undefined + return s?.message?.trim() ?? '' +} + +const EMOJI_HEAD = /^\p{Extended_Pictographic}/u + +/** + * Returns the first emoji sequence at the start of the status message (after trim), or undefined. + * Handles variation selectors, skin tones, and ZWJ sequences (e.g. family emoji). + * @public + */ +export function extractLeadingStatusEmoji (raw: string | undefined): string | undefined { + const text = raw?.trim() ?? '' + if (text === '') return undefined + + try { + const seg = new Intl.Segmenter(undefined, { granularity: 'grapheme' }) + const segments = [...seg.segment(text)].map((x) => x.segment) + if (segments.length === 0) return undefined + if (!EMOJI_HEAD.test(segments[0])) return undefined + + let out = segments[0] + let i = 1 + while (i < segments.length) { + const s = segments[i] + if (s === '\u200D' && i + 1 < segments.length && EMOJI_HEAD.test(segments[i + 1])) { + out += s + segments[i + 1] + i += 2 + continue + } + if (s === '\uFE0F' || /^[\u{1F3FB}-\u{1F3FF}]$/u.test(s)) { + out += s + i++ + continue + } + break + } + return out + } catch { + const m = text.match(/^(\p{Extended_Pictographic})/u) + return m?.[1] + } +} diff --git a/plugins/workbench-resources/src/components/AccountPopup.svelte b/plugins/workbench-resources/src/components/AccountPopup.svelte index eca320f5ddf..3da17fceca6 100644 --- a/plugins/workbench-resources/src/components/AccountPopup.svelte +++ b/plugins/workbench-resources/src/components/AccountPopup.svelte @@ -13,8 +13,13 @@ // limitations under the License. -->