Signed-off-by: budaeva <irina.budaeva@xored.com>
This commit is contained in:
budaeva 2022-06-21 14:55:17 +07:00 committed by GitHub
parent 0555aa402e
commit c98a80ff17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 521 additions and 38 deletions

View File

@ -2,6 +2,10 @@
## 0.6.29 (upcoming)
Chunter:
- Reactions on messages
## 0.6.28
Core:

View File

@ -243,6 +243,7 @@ specifiers:
dotenv-webpack: ^7.0.2
elastic-apm-node: ~3.26.0
email-addresses: ^5.0.0
emoji-regex: ^10.1.0
esbuild: ^0.12.26
eslint: ^7.32.0
eslint-config-standard-with-typescript: ^21.0.1
@ -543,6 +544,7 @@ dependencies:
dotenv-webpack: 7.1.0_webpack@5.73.0
elastic-apm-node: 3.26.0
email-addresses: 5.0.0
emoji-regex: 10.1.0
esbuild: 0.12.29
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_99a5fe2f2ae1dc64d6b59974c931eb2a
@ -565,7 +567,7 @@ dependencies:
koa-bodyparser: 4.3.0
koa-router: 10.1.1
lexorank: 1.0.4
libphonenumber-js: 1.10.6
libphonenumber-js: 1.10.7
mime-types: 2.1.35
mini-css-extract-plugin: 2.6.0_webpack@5.73.0
minio: 7.0.28
@ -4485,10 +4487,26 @@ packages:
engines: {node: '>=10'}
dev: false
/emoji-name-map/1.2.9:
resolution: {integrity: sha512-MSM8y6koSqh/2uEMI2VoKA+Ac0qL5RkgFGP/pzL6n5FOrOJ7FOZFxgs7+uNpqA+AT+WmdbMPXkd3HnFXXdz4AA==}
dependencies:
emojilib: 2.4.0
iterate-object: 1.3.4
map-o: 2.0.10
dev: false
/emoji-regex/10.1.0:
resolution: {integrity: sha512-xAEnNCT3w2Tg6MA7ly6QqYJvEoY1tm9iIjJ3yMKK9JPlWuRHAMoe5iETwQnx3M9TVbFMfsrBgWKR+IsmswwNjg==}
dev: false
/emoji-regex/8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: false
/emojilib/2.4.0:
resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==}
dev: false
/emojis-list/3.0.0:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
engines: {node: '>= 4'}
@ -6175,6 +6193,10 @@ packages:
istanbul-lib-report: 3.0.0
dev: false
/iterate-object/1.3.4:
resolution: {integrity: sha512-4dG1D1x/7g8PwHS9aK6QV5V94+ZvyP4+d19qDv43EzImmrndysIl4prmJ1hWWIGCqrZHyaHBm6BSEWHOLnpoNw==}
dev: false
/jest-changed-files/27.5.1:
resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@ -6896,8 +6918,8 @@ packages:
resolution: {integrity: sha512-CMgA8AMJIX/QfoYHKyjg0hv9W1SGL2xRkt0uLyhT9xKKRj73fHi+IhsrB3W36wwk4I0iz8YlKHfdW14QDwerMA==}
dev: false
/libphonenumber-js/1.10.6:
resolution: {integrity: sha512-CIjT100/SmntsUjsLVs2t3ufeN4KdNXUxhD07tH153pdbaCWuAjv0jK/gPuywR3IImB/U/MQM+x9RfhMs5XZiA==}
/libphonenumber-js/1.10.7:
resolution: {integrity: sha512-jZXLCCWMe1b/HXkjiLeYt2JsytZMcqH26jLFIdzFDFF0xvSUWrYKyvPlyPG+XJzEyKUFbcZxLdWGMwQsWaHDxQ==}
dev: false
/lilconfig/2.0.5:
@ -7041,6 +7063,12 @@ packages:
tmpl: 1.0.5
dev: false
/map-o/2.0.10:
resolution: {integrity: sha512-BxazE81fVByHWasyXhqKeo2m7bFKYu+ZbEfiuexMOnklXW+tzDvnlTi/JaklEeuuwqcqJzPaf9q+TWptSGXeLg==}
dependencies:
iterate-object: 1.3.4
dev: false
/mapcap/1.0.0:
resolution: {integrity: sha512-KcNlZSlFPx+r1jYZmxEbTVymG+dIctf10WmWkuhrhrblM+KMoF77HelwihL5cxYlORye79KoR4IlOOk99lUJ0g==}
dev: false
@ -10372,6 +10400,8 @@ packages:
dependencies:
'@typescript-eslint/eslint-plugin': 5.27.0_738fa17fa57f8a69ace69c90e5cfa1d5
'@typescript-eslint/parser': 5.27.0_eslint@7.32.0+typescript@4.7.2
emoji-regex: 10.1.0
emojis-list: 3.0.0
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_99a5fe2f2ae1dc64d6b59974c931eb2a
eslint-plugin-import: 2.26.0_c21022bc9feaeb7b200d3d631eeae46c
@ -14375,7 +14405,7 @@ packages:
dev: false
file:projects/text-editor.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c:
resolution: {integrity: sha512-baB/9mrVbC1HBM4EqGCpqQ+C6aUzGbYCUeP8x3O1xp6YUkJrl4XcH6o7aZNtNDgJRvSDPdDAUg5oP/++gud68w==, tarball: file:projects/text-editor.tgz}
resolution: {integrity: sha512-AglZab4XONA+NpPrSdMKnpddn+3d+bxZZecMlXLhsg5g+KuD15T9KRL9B/SdTWFWvhJKsPVr5uCPj6CbsE8Y6g==, tarball: file:projects/text-editor.tgz}
id: file:projects/text-editor.tgz
name: '@rush-temp/text-editor'
version: 0.0.0
@ -14393,6 +14423,8 @@ packages:
'@types/prosemirror-model': 1.16.2
'@typescript-eslint/eslint-plugin': 5.27.0_738fa17fa57f8a69ace69c90e5cfa1d5
'@typescript-eslint/parser': 5.27.0_eslint@7.32.0+typescript@4.7.2
emoji-name-map: 1.2.9
emoji-regex: 10.1.0
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_99a5fe2f2ae1dc64d6b59974c931eb2a
eslint-plugin-import: 2.26.0_c21022bc9feaeb7b200d3d631eeae46c
@ -14489,7 +14521,7 @@ packages:
eslint-plugin-promise: 5.2.0_eslint@7.32.0
fast-equals: 2.0.4
got: 11.8.5
libphonenumber-js: 1.10.6
libphonenumber-js: 1.10.7
mime-types: 2.1.35
minio: 7.0.28
mongodb: 4.6.0
@ -14662,6 +14694,8 @@ packages:
dependencies:
'@typescript-eslint/eslint-plugin': 5.27.0_738fa17fa57f8a69ace69c90e5cfa1d5
'@typescript-eslint/parser': 5.27.0_eslint@7.32.0+typescript@4.7.2
emoji-regex: 10.1.0
emojis-list: 3.0.0
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_99a5fe2f2ae1dc64d6b59974c931eb2a
eslint-plugin-import: 2.26.0_c21022bc9feaeb7b200d3d631eeae46c

View File

@ -22,6 +22,7 @@ import type {
Comment,
DirectMessage,
Message,
Reaction,
SavedMessages,
ThreadMessage
} from '@anticrm/chunter'
@ -90,6 +91,9 @@ export class TChunterMessage extends TAttachedDoc implements ChunterMessage {
@Prop(TypeTimestamp(), chunter.string.Edit)
editedOn?: Timestamp
@Prop(Collection(chunter.class.Reaction), chunter.string.Reactions)
reactions?: number
}
@Model(chunter.class.ThreadMessage, chunter.class.ChunterMessage)
@ -114,6 +118,18 @@ export class TMessage extends TChunterMessage implements Message {
lastReply?: Timestamp
}
@Model(chunter.class.Reaction, core.class.AttachedDoc, DOMAIN_CHUNTER)
export class TReaction extends TAttachedDoc implements Reaction {
@Prop(TypeString(), chunter.string.Emoji)
emoji!: string
@Prop(TypeRef(core.class.Account), chunter.string.CreateBy)
createBy!: Ref<Account>
declare attachedTo: Ref<ChunterMessage>
declare attachedToClass: Ref<Class<ChunterMessage>>
}
@Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
@UX(chunter.string.Comment)
export class TComment extends TAttachedDoc implements Comment {
@ -148,7 +164,8 @@ export function createModel (builder: Builder): void {
TComment,
TBacklink,
TDirectMessage,
TSavedMessages
TSavedMessages,
TReaction
)
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]

View File

@ -65,7 +65,9 @@ export default mergeIds(chunterId, chunter, {
LastMessage: '' as IntlString,
PinnedMessages: '' as IntlString,
SavedMessages: '' as IntlString,
ThreadMessage: '' as IntlString
ThreadMessage: '' as IntlString,
Reactions: '' as IntlString,
Emoji: '' as IntlString
},
viewlet: {
Chat: '' as Ref<ViewletDescriptor>

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "anticrm",
"lockfileVersion": 2,
"requires": true,
"packages": {}
}

View File

@ -13,6 +13,13 @@
"BulletedList": "Bulleted list",
"Blockquote": "Blockquote",
"Code": "Code",
"CodeBlock": "Code block"
"CodeBlock": "Code block",
"GettingWorkDone": "Getting work done",
"Smileys": "Smileys",
"Nature": "Nature",
"Symbols": "Symbols",
"TravelAndPlaces": "Travel & Places",
"Objects": "Objects",
"Food": "Food"
}
}

View File

@ -13,6 +13,13 @@
"BulletedList": "Маркированный список",
"Blockquote": "Цитата",
"Code": "Код",
"CodeBlock": "Кодовый блок"
"CodeBlock": "Кодовый блок",
"GettingWorkDone": "Для работы",
"Smileys": "Смайлики",
"Nature": "Природа",
"Symbols": "Символы",
"TravelAndPlaces": "Путешествия & Места",
"Objects": "Объекты",
"Food": "Еда"
}
}

View File

@ -47,6 +47,7 @@
"@tiptap/suggestion": "~2.0.0-beta.92",
"prosemirror-model": "~1.17.0",
"@tiptap/extension-task-list": "~2.0.0-beta.26",
"@tiptap/extension-task-item": "~2.0.0-beta.32"
"@tiptap/extension-task-item": "~2.0.0-beta.32",
"emoji-regex": "^10.1.0"
}
}

View File

@ -0,0 +1,193 @@
<script lang="ts">
import emojiRegex from 'emoji-regex'
import { createEventDispatcher } from 'svelte'
import { IntlString } from '@anticrm/platform'
import { AnySvelteComponent, Label } from '@anticrm/ui'
import Tooltip from '@anticrm/ui/src/components/Tooltip.svelte'
import Emoji from './icons/Emoji.svelte'
import Food from './icons/Food.svelte'
import Nature from './icons/Nature.svelte'
import Objects from './icons/Objects.svelte'
import Places from './icons/Places.svelte'
import Symbols from './icons/Symbols.svelte'
import Work from './icons/Work.svelte'
import plugin from '../plugin'
let div: HTMLDivElement
const regex = emojiRegex()
function getEmojis (startCode: number, endCode: number, postfix?: number[]): (string | undefined)[] {
return [...Array(endCode - startCode + 1).keys()].map((v) => {
const str = postfix ? String.fromCodePoint(v + startCode, ...postfix) : String.fromCodePoint(v + startCode)
if ([...str.matchAll(regex)].length > 0) return str
return undefined
})
}
interface Category {
id: string
label: IntlString
emojis: (string | undefined)[]
icon: AnySvelteComponent
}
const categories: Category[] = [
{
id: 'work',
label: plugin.string.GettingWorkDone,
emojis: [
String.fromCodePoint(0x1f440),
String.fromCodePoint(0x2705),
String.fromCodePoint(0x274c),
String.fromCodePoint(0x2795),
String.fromCodePoint(0x2796),
String.fromCodePoint(0x2757),
String.fromCodePoint(0x0031, 0xfe0f, 0x20e3),
String.fromCodePoint(0x0032, 0xfe0f, 0x20e3),
String.fromCodePoint(0x0033, 0xfe0f, 0x20e3),
String.fromCodePoint(0x1f44b, 0x1f3fc),
String.fromCodePoint(0x1f44d, 0x1f3fc),
String.fromCodePoint(0x1f44c, 0x1f3fc)
],
icon: Work
},
{
id: 'smileys',
label: plugin.string.Smileys,
emojis: [...getEmojis(0x1f600, 0x1f64f), ...getEmojis(0x1f90c, 0x1f92f)],
icon: Emoji
},
{
id: 'nature',
label: plugin.string.Nature,
emojis: [
...getEmojis(0x1f408, 0x1f43e),
...getEmojis(0x1f980, 0x1f9ae),
...getEmojis(0x1f330, 0x1f343),
...getEmojis(0x1f300, 0x1f320),
...getEmojis(0x1f324, 0x1f32c, [0xfe0f]),
...getEmojis(0x2600, 0x2604, [0xfe0f])
],
icon: Nature
},
{
id: 'travels',
label: plugin.string.TravelAndPlaces,
emojis: [...getEmojis(0x1f5fb, 0x1f5ff), ...getEmojis(0x1f3e0, 0x1f3f0), ...getEmojis(0x1f680, 0x1f6a3)],
icon: Places
},
{
id: 'food',
label: plugin.string.Food,
emojis: [...getEmojis(0x1f345, 0x1f37f), ...getEmojis(0x1f32d, 0x1f32f)],
icon: Food
},
{
id: 'objects',
label: plugin.string.Objects,
emojis: [...getEmojis(0x1f4b6, 0x1f4fc)],
icon: Objects
},
{
id: 'symbols',
label: plugin.string.Symbols,
emojis: [
...getEmojis(0x00a9, 0x25fc, [0xfe0f]),
...getEmojis(0x2764, 0x2b07, [0xfe0f]),
...getEmojis(0x0023, 0x0039, [0xfe0f, 0x20e3]),
...getEmojis(0x1f532, 0x1f53d)
],
icon: Symbols
}
]
const dispatch = createEventDispatcher()
const headerHeight = 55
function handleScrollToCategory (categoryId: string) {
const offset = document.getElementById(categoryId)?.offsetTop
if (offset) div.scrollTo(0, offset - headerHeight)
}
let currentCategory = categories[0]
const padding = 16
function handleScroll () {
const divTop = div?.getBoundingClientRect().top
const categoryDivs = div.getElementsByClassName('categoryName')
const i = Array.from(categoryDivs).findIndex((element) => {
if (element?.nodeType === Node.ELEMENT_NODE) {
const elementTop = element?.getBoundingClientRect().top
if (elementTop >= divTop + padding) {
return true
}
}
return false
})
let firstVisibleCategory: Element | null
if (i > 0) {
firstVisibleCategory = categoryDivs.item(i - 1)
} else {
firstVisibleCategory = categoryDivs.item(categoryDivs.length - 1)
}
if (firstVisibleCategory !== null) {
currentCategory = categories.find((c) => c.id === firstVisibleCategory!.id) ?? categories[0]
}
}
</script>
<div class="antiPopup antiPopup-withHeader pb-3 popup">
<div class="flex-between ml-4 pt-2 pb-2 mr-4 header">
{#each categories as category}
<Tooltip label={category.label}>
<div
class="flex-grow pt-2 pb-2 pl-2 pr-2 element"
class:selected={currentCategory === category}
on:click={() => handleScrollToCategory(category.id)}
>
<svelte:component this={category.icon} size={'large'} opacity={currentCategory === category ? '1' : '0.3'} />
</div>
</Tooltip>
{/each}
</div>
<div class="flex-col vScroll" bind:this={div} on:scroll={handleScroll}>
<div class="w-85 flex-col">
{#each categories as category}
<div class="ap-header">
<div id={category.id} class="ap-caption categoryName"><Label label={category.label} /></div>
</div>
<div class="palette ml-4">
{#each category.emojis as emoji}
{#if emoji !== undefined}
<div class="p-1 element" on:click={() => dispatch('close', emoji)}>{emoji}</div>
{/if}
{/each}
</div>
{/each}
</div>
</div>
</div>
<style lang="scss">
.popup {
height: 25rem;
}
.palette {
display: grid;
grid-template-columns: repeat(8, 1fr);
font-size: x-large;
}
.element {
&:hover {
background-color: var(--popup-bg-hover);
}
&.selected {
background-color: var(--popup-bg-hover);
}
}
.header {
justify-content: start;
border-bottom: 1px solid var(--divider-color);
}
</style>

View File

@ -38,6 +38,7 @@
import MentionList from './MentionList.svelte'
import { SvelteRenderer } from './SvelteRenderer'
import TextEditor from './TextEditor.svelte'
import EmojiPopup from './EmojiPopup.svelte'
import LinkPopup from './LinkPopup.svelte'
const dispatch = createEventDispatcher()
@ -83,7 +84,18 @@
{
label: textEditorPlugin.string.Emoji,
icon: Emoji,
action: () => {},
action: (element) => {
showPopup(
EmojiPopup,
{},
element,
(emoji) => {
if (!emoji) return
textEditor.insertText(emoji)
},
() => {}
)
},
order: 3000
},
{

View File

@ -15,11 +15,12 @@
<script lang="ts">
import { IntlString } from '@anticrm/platform'
import { ScrollBox } from '@anticrm/ui'
import { ScrollBox, showPopup } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import Emoji from './icons/Emoji.svelte'
import GIF from './icons/GIF.svelte'
import TextStyle from './icons/TextStyle.svelte'
import EmojiPopup from './EmojiPopup.svelte'
import TextEditor from './TextEditor.svelte'
import textEditorPlugin from '../plugin'
@ -38,6 +39,13 @@
export function focus (): void {
textEditor.focus()
}
function openEmojiPopup (ev: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) {
showPopup(EmojiPopup, {}, ev.target as HTMLElement, (emoji) => {
if (!emoji) return
textEditor.insertText(emoji)
})
}
</script>
<div class="ref-container">
@ -81,7 +89,7 @@
{#if showButtons}
<div class="buttons">
<div class="tool"><TextStyle size={'large'} /></div>
<div class="tool"><Emoji size={'large'} /></div>
<div class="tool" on:click={openEmojiPopup}><Emoji size={'large'} /></div>
<div class="tool"><GIF size={'large'} /></div>
<div class="flex-grow">
<slot />

View File

@ -1,10 +1,12 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let opacity: string = '1'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path
{opacity}
d="M10 2C14.4183 2 18 5.58172 18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2ZM10 3C6.13401 3 3 6.13401 3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3ZM7.15467 12.4273C8.66416 13.9463 11.0877 14.0045 12.6671 12.5961L12.8453 12.4273C13.04 12.2314 13.3566 12.2304 13.5524 12.4251C13.7265 12.5981 13.7467 12.8674 13.6123 13.0627L13.5547 13.1322L13.5323 13.1545C11.5691 15.1054 8.39616 15.0953 6.44533 13.1322C6.25069 12.9363 6.25169 12.6197 6.44757 12.4251C6.64344 12.2304 6.96002 12.2314 7.15467 12.4273ZM12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5ZM7.5 7.5C8.05228 7.5 8.5 7.94772 8.5 8.5C8.5 9.05228 8.05228 9.5 7.5 9.5C6.94772 9.5 6.5 9.05228 6.5 8.5C6.5 7.94772 6.94772 7.5 7.5 7.5Z"
/>
</svg>

View File

@ -0,0 +1,20 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let opacity: string = '1'
</script>
<svg class="svg-{size}" fill="none" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<g {opacity}>
<path d="M7.5 17.5H12.5" stroke="currentColor" stroke-linecap="round" />
<path d="M10 17.5V14.1667" stroke="currentColor" stroke-linecap="round" />
<path
d="M4.6665 7.7381C4.6665 7.66499 4.6667 7.64911 4.66729 7.63863C4.68156 7.38455 4.88439 7.18172 5.13847 7.16745C5.14894 7.16687 5.16482 7.16667 5.23793 7.16667H14.7617C14.8349 7.16667 14.8507 7.16687 14.8612 7.16745C15.1153 7.18172 15.3181 7.38455 15.3324 7.63863C15.333 7.64911 15.3332 7.66498 15.3332 7.7381V8.33333C15.3332 11.2789 12.9454 13.6667 9.99984 13.6667C7.05432 13.6667 4.6665 11.2789 4.6665 8.33333V7.7381Z"
stroke="currentColor"
stroke-linecap="round"
/>
<path
d="M9.51924 9.86264C9.44338 10.1282 9.59712 10.4049 9.86264 10.4808C10.1282 10.5566 10.4049 10.4029 10.4808 10.1374L9.51924 9.86264ZM11.3972 5.10987L10.9164 4.9725L11.3972 5.10987ZM12.5774 3.80235L12.7631 4.26659H12.7631L12.5774 3.80235ZM11.7511 4.24292L11.38 3.90789L11.7511 4.24292ZM10.4808 10.1374L11.8779 5.24723L10.9164 4.9725L9.51924 9.86264L10.4808 10.1374ZM12.7631 4.26659L16.019 2.96424L15.6476 2.03576L12.3918 3.33812L12.7631 4.26659ZM11.8779 5.24723C12.0238 4.73685 12.0628 4.64387 12.1223 4.57796L11.38 3.90789C11.1285 4.18646 11.0352 4.55661 10.9164 4.9725L11.8779 5.24723ZM12.3918 3.33812C11.9902 3.49876 11.6314 3.62933 11.38 3.90789L12.1223 4.57796C12.1818 4.51204 12.2703 4.46373 12.7631 4.26659L12.3918 3.33812Z"
stroke="currentColor"
/>
</g>
</svg>

View File

@ -0,0 +1,18 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let opacity: string = '1'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g {opacity}>
<path
d="M12.0448 9.07344L12.5026 8.87234L12.0448 9.07344ZM7.49167 16.6515L7.52884 17.1501L7.49167 16.6515ZM7.45449 16.1529C7.332 16.162 7.20817 16.1667 7.08317 16.1667V17.1667C7.23301 17.1667 7.38164 17.1611 7.52884 17.1501L7.45449 16.1529ZM7.08317 16.1667C4.36777 16.1667 2.1665 13.9654 2.1665 11.25H1.1665C1.1665 14.5177 3.81549 17.1667 7.08317 17.1667V16.1667ZM2.1665 11.25C2.1665 8.5346 4.36777 6.33333 7.08317 6.33333V5.33333C3.81549 5.33333 1.1665 7.98231 1.1665 11.25H2.1665ZM7.08317 6.33333C9.09506 6.33333 10.8258 7.54181 11.587 9.27454L12.5026 8.87234C11.5876 6.78951 9.50597 5.33333 7.08317 5.33333V6.33333ZM13.3332 10.5C14.898 10.5 16.1665 11.7685 16.1665 13.3333H17.1665C17.1665 11.2162 15.4503 9.5 13.3332 9.5V10.5ZM16.1665 13.3333C16.1665 14.8981 14.898 16.1667 13.3332 16.1667V17.1667C15.4503 17.1667 17.1665 15.4504 17.1665 13.3333H16.1665ZM13.3332 16.1667H7.50743V17.1667H13.3332V16.1667ZM7.50743 16.1667C7.77939 16.1667 7.99984 16.3871 7.99984 16.6591H6.99984C6.99984 16.9394 7.22708 17.1667 7.50743 17.1667V16.1667ZM11.587 9.27454C11.8757 9.93169 12.5127 10.5 13.3332 10.5V9.5C13.0071 9.5 12.675 9.26494 12.5026 8.87234L11.587 9.27454ZM7.52884 17.1501C7.24371 17.1714 6.99984 16.9459 6.99984 16.6591H7.99984C7.99984 16.3634 7.74844 16.131 7.45449 16.1529L7.52884 17.1501Z"
{fill}
/>
<path
d="M16.2794 11.2796C16.6988 10.8602 17.0239 10.3561 17.2328 9.80104C17.4417 9.24595 17.5297 8.65266 17.4909 8.06082C17.4521 7.46899 17.2874 6.89225 17.0078 6.36918C16.7283 5.84611 16.3402 5.38876 15.8697 5.0277C15.3991 4.66664 14.8569 4.41019 14.2793 4.27551C13.7017 4.14083 13.102 4.13102 12.5203 4.24673C11.9386 4.36244 11.3883 4.601 10.9062 4.94647C10.4241 5.29194 10.0213 5.73636 9.72473 6.25"
stroke="currentColor"
/>
</g>
</svg>

View File

@ -0,0 +1,12 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let opacity: string = '1'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path
{opacity}
d="M9.50963 17.0277L9.44387 17.5234L9.50963 17.0277ZM8.62785 16.7027L8.27419 17.0562L8.62785 16.7027ZM6.92392 12.2754L6.61606 12.6694L6.92392 12.2754ZM7.65746 13.164L7.19335 13.3501L7.65746 13.164ZM7.44487 12.7262L7.84461 12.4258L7.44487 12.7262ZM10.4904 17.0277L10.5561 17.5234L10.4904 17.0277ZM11.3722 16.7027L11.7258 17.0562L11.3722 16.7027ZM13.0761 12.2754L12.7682 11.8814L13.0761 12.2754ZM12.5551 12.7262L12.9549 13.0265L12.5551 12.7262ZM14.5 8.33333C14.5 9.77433 13.8232 11.057 12.7682 11.8814L13.3839 12.6694C14.6711 11.6636 15.5 10.0951 15.5 8.33333H14.5ZM10 3.83333C12.4853 3.83333 14.5 5.84805 14.5 8.33333H15.5C15.5 5.29577 13.0376 2.83333 10 2.83333V3.83333ZM5.5 8.33333C5.5 5.84805 7.51472 3.83333 10 3.83333V2.83333C6.96243 2.83333 4.5 5.29577 4.5 8.33333H5.5ZM7.23179 11.8814C6.17675 11.057 5.5 9.77433 5.5 8.33333H4.5C4.5 10.0951 5.32894 11.6636 6.61606 12.6694L7.23179 11.8814ZM8.748 15.3687C8.63971 14.5487 8.42923 13.7455 8.12156 12.978L7.19335 13.3501C7.46999 14.0402 7.65924 14.7623 7.75661 15.4996L8.748 15.3687ZM10.4246 16.5321C10.1428 16.5695 9.85719 16.5695 9.57538 16.5321L9.44387 17.5234C9.81297 17.5724 10.187 17.5724 10.5561 17.5234L10.4246 16.5321ZM11.8784 12.978C11.5708 13.7455 11.3603 14.5487 11.252 15.3687L12.2434 15.4996C12.3408 14.7623 12.53 14.0402 12.8066 13.3501L11.8784 12.978ZM7.75661 15.4996C7.80026 15.8302 7.8381 16.1235 7.90038 16.362C7.96682 16.6164 8.07123 16.8531 8.27419 17.0562L8.98151 16.3493C8.94478 16.3125 8.90671 16.2578 8.86794 16.1093C8.82501 15.945 8.79492 15.7239 8.748 15.3687L7.75661 15.4996ZM9.57538 16.5321C9.34646 16.5017 9.23632 16.4853 9.15156 16.458C9.09245 16.4389 9.04706 16.4149 8.98151 16.3493L8.27419 17.0562C8.6486 17.4308 9.03105 17.4686 9.44387 17.5234L9.57538 16.5321ZM6.61606 12.6694C6.76644 12.7869 6.86761 12.8661 6.94183 12.9289C7.01729 12.9928 7.0396 13.0192 7.04512 13.0265L7.84461 12.4258C7.77064 12.3274 7.67964 12.2433 7.58807 12.1658C7.49527 12.0872 7.37571 11.9939 7.23179 11.8814L6.61606 12.6694ZM8.12156 12.978C8.04043 12.7756 7.97211 12.5955 7.84461 12.4258L7.04512 13.0265C7.07659 13.0684 7.09701 13.1097 7.19335 13.3501L8.12156 12.978ZM10.5561 17.5234C10.969 17.4686 11.3514 17.4308 11.7258 17.0562L11.0185 16.3493C10.9529 16.4149 10.9076 16.4389 10.8484 16.458C10.7637 16.4853 10.6535 16.5017 10.4246 16.5321L10.5561 17.5234ZM11.252 15.3687C11.2051 15.7239 11.175 15.945 11.1321 16.1093C11.0933 16.2578 11.0552 16.3125 11.0185 16.3493L11.7258 17.0562C11.9288 16.8531 12.0332 16.6164 12.0996 16.362C12.1619 16.1235 12.1997 15.8302 12.2434 15.4996L11.252 15.3687ZM12.7682 11.8814C12.6243 11.9939 12.5047 12.0872 12.4119 12.1658C12.3204 12.2433 12.2294 12.3274 12.1554 12.4258L12.9549 13.0265C12.9604 13.0192 12.9827 12.9928 13.0582 12.9289C13.1324 12.8661 13.2336 12.7869 13.3839 12.6694L12.7682 11.8814ZM12.8066 13.3501C12.903 13.1097 12.9234 13.0684 12.9549 13.0265L12.1554 12.4258C12.0279 12.5955 11.9596 12.7756 11.8784 12.978L12.8066 13.3501Z"
/>
</svg>

View File

@ -0,0 +1,12 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let opacity: string = '1'
const fill: string = 'currentColor'
</script>
<svg xmlns="http://www.w3.org/2000/svg" class="svg-{size}" {fill} viewBox="0 0 16 16">
<path
{opacity}
d="M 8 1.320313 L 0.660156 8.132813 L 1.339844 8.867188 L 2 8.253906 L 2 14 L 7 14 L 7 9 L 9 9 L 9 14 L 14 14 L 14 8.253906 L 14.660156 8.867188 L 15.339844 8.132813 Z M 8 2.679688 L 13 7.328125 L 13 13 L 10 13 L 10 8 L 6 8 L 6 13 L 3 13 L 3 7.328125 Z"
/></svg
>

View File

@ -0,0 +1,12 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let opacity: string = '1'
const fill: string = 'currentColor'
</script>
<svg xmlns="http://www.w3.org/2000/svg" class="svg-{size}" {fill} viewBox="0 0 16 16">
<path
{opacity}
d="M 14.5 2.792969 L 5.5 11.792969 L 1.851563 8.148438 L 1.5 7.792969 L 0.792969 8.5 L 1.148438 8.851563 L 5.5 13.207031 L 15.207031 3.5 Z"
/></svg
>

View File

@ -0,0 +1,16 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
export let opacity: string = '1'
</script>
<svg class="svg-{size}" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g {opacity}>
<path
d="M14.1665 7.5L11.789 11.0662C11.3631 11.7051 10.4059 11.6455 10.0625 10.9587L9.93713 10.7079C9.59375 10.0212 8.63657 9.96157 8.21066 10.6004L5.83317 14.1667"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
<rect x="2.5" y="2.5" width="15" height="15" rx="2" stroke="currentColor" />
</g>
</svg>

View File

@ -23,6 +23,7 @@ export { default as StyledTextBox } from './components/StyledTextBox.svelte'
export { default as StyledTextArea } from './components/StyledTextArea.svelte'
export { default as StyledTextEditor } from './components/StyledTextEditor.svelte'
export { default as TextEditor } from './components/TextEditor.svelte'
export { default as EmojiPopup } from './components/EmojiPopup.svelte'
export { default } from './plugin'
export * from './types'

View File

@ -45,6 +45,13 @@ export default plugin(textEditorId, {
BulletedList: '' as IntlString,
Blockquote: '' as IntlString,
Code: '' as IntlString,
CodeBlock: '' as IntlString
CodeBlock: '' as IntlString,
GettingWorkDone: '' as IntlString,
Smileys: '' as IntlString,
Nature: '' as IntlString,
Symbols: '' as IntlString,
TravelAndPlaces: '' as IntlString,
Food: '' as IntlString,
Objects: '' as IntlString
}
})

View File

@ -113,7 +113,7 @@
},
{
lookup: {
_id: { attachments: attachment.class.Attachment },
_id: { attachments: attachment.class.Attachment, reactions: chunter.class.Reaction },
createBy: core.class.Account
}
}

View File

@ -15,7 +15,7 @@
<script lang="ts">
import { Attachment } from '@anticrm/attachment'
import { AttachmentList, AttachmentRefInput } from '@anticrm/attachment-resources'
import type { ChunterMessage, Message } from '@anticrm/chunter'
import type { ChunterMessage, Message, Reaction } from '@anticrm/chunter'
import { Employee, EmployeeAccount } from '@anticrm/contact'
import { EmployeePresenter } from '@anticrm/contact-resources'
import { Ref, WithLookup, getCurrentAccount } from '@anticrm/core'
@ -35,6 +35,7 @@
} from '@anticrm/ui'
import { Action } from '@anticrm/view'
import { getActions, LinkPresenter } from '@anticrm/view-resources'
import { EmojiPopup } from '@anticrm/text-editor'
import { createEventDispatcher } from 'svelte'
import { AddMessageToSaved, DeleteMessageFromSaved, UnpinMessage } from '../index'
import chunter from '../plugin'
@ -62,7 +63,7 @@
const client = getClient()
const dispatch = createEventDispatcher()
const reactions: boolean = false
$: reactions = message.$lookup?.reactions as Reaction[] | undefined
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
@ -181,6 +182,45 @@
else AddMessageToSaved(message)
}
function openEmojiPalette (ev: Event) {
showPopup(
EmojiPopup,
{},
ev.target as HTMLElement,
async (emoji) => {
if (!emoji) return
const me = getCurrentAccount()._id
const reaction = reactions?.find((r) => r.emoji === emoji && r.createBy === me)
if (!reaction) {
await client.createDoc(chunter.class.Reaction, message.space, {
attachedTo: message._id,
attachedToClass: chunter.class.ChunterMessage,
emoji,
createBy: me,
collection: 'reactions'
})
} else {
await client.removeDoc(chunter.class.Reaction, message.space, reaction._id)
}
},
() => {}
)
}
async function removeReaction (ev: CustomEvent) {
if (!ev.detail) return
const me = getCurrentAccount()._id
const reaction = await client.findOne(chunter.class.Reaction, {
attachedTo: message._id,
emoji: ev.detail,
createBy: me
})
if (reaction?._id) {
client.removeDoc(chunter.class.Reaction, reaction.space, reaction._id)
}
}
$: parentMessage = message as Message
$: hasReplies = (parentMessage?.replies?.length ?? 0) > 0
@ -247,9 +287,9 @@
<LinkPresenter {link} />
{/each}
{/if}
{#if reactions || (!thread && hasReplies)}
{#if reactions?.length || (!thread && hasReplies)}
<div class="footer flex-col">
{#if reactions}<Reactions />{/if}
{#if reactions?.length}<Reactions {reactions} on:remove={removeReaction} />{/if}
{#if !thread && hasReplies}
<Replies message={parentMessage} on:click={openThread} />
{/if}
@ -278,7 +318,7 @@
/>
</div>
<!-- <div class="tool"><ActionIcon icon={Share} size={'medium'}/></div> -->
<div class="tool"><ActionIcon icon={Emoji} size={'medium'} /></div>
<div class="tool"><ActionIcon icon={Emoji} size={'medium'} action={openEmojiPalette} /></div>
</div>
</div>

View File

@ -13,26 +13,42 @@
// limitations under the License.
-->
<script lang="ts">
import type { AnySvelteComponent } from '@anticrm/ui'
import Check from './icons/Check.svelte'
import Heart from './icons/Heart.svelte'
import { Reaction } from '@anticrm/chunter'
import { Account, Ref } from '@anticrm/core'
import { Tooltip } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import ReactionsTooltip from './ReactionsTooltip.svelte'
interface Reaction {
icon: AnySvelteComponent
count: number
export let reactions: Reaction[] = []
const dispatch = createEventDispatcher()
let reactionsAccounts: Map<string, Ref<Account>[]> = new Map()
$: {
reactionsAccounts.clear()
reactions.forEach((r) => {
let accounts = reactionsAccounts.get(r.emoji)
accounts = accounts ? [...accounts, r.createBy] : [r.createBy]
reactionsAccounts.set(r.emoji, accounts)
})
reactionsAccounts = reactionsAccounts
}
export let reactions: Reaction[] = [
{ icon: Check, count: 3 },
{ icon: Heart, count: 10 }
]
</script>
<div class="container">
{#each reactions as reaction}
<div class="flex-row-center reaction">
<svelte:component this={reaction.icon} size={'medium'} />
<div class="caption-color counter">{reaction.count}</div>
{#each [...reactionsAccounts] as [emoji, accounts]}
<div class="reaction over-underline">
<Tooltip component={ReactionsTooltip} props={{ reactionAccounts: accounts }}>
<div
class="flex-row-center"
on:click={() => {
dispatch('remove', emoji)
}}
>
<div>{emoji}</div>
<div class="caption-color counter">{accounts.length}</div>
</div>
</Tooltip>
</div>
{/each}
</div>

View File

@ -0,0 +1,24 @@
<script lang="ts">
import { Account, Ref } from '@anticrm/core'
import contact, { EmployeeAccount, formatName } from '@anticrm/contact'
import { getClient } from '@anticrm/presentation'
export let reactionAccounts: Ref<Account>[]
const client = getClient()
let accountNames: string[] = []
async function getAccountNames (reactionAccounts: Ref<EmployeeAccount>[]) {
client
.findAll(contact.class.EmployeeAccount, { _id: { $in: reactionAccounts } })
.then((res) => (accountNames = res.map((a) => a.name)))
}
$: getAccountNames(reactionAccounts as Ref<EmployeeAccount>[])
</script>
{#each accountNames as name}
<div>
{formatName(name)}
</div>
{/each}

View File

@ -71,7 +71,7 @@
const lastViews = notificationClient.getLastViews()
const lookup = {
_id: { attachments: attachment.class.Attachment },
_id: { attachments: attachment.class.Attachment, reactions: chunter.class.Reaction },
createBy: core.class.Account
}
@ -97,7 +97,7 @@
},
{
lookup: {
_id: { attachments: attachment.class.Attachment },
_id: { attachments: attachment.class.Attachment, reactions: chunter.class.Reaction },
createBy: core.class.Account
}
}

View File

@ -49,6 +49,7 @@ export interface ChunterMessage extends AttachedDoc {
createBy: Ref<Account>
createOn: Timestamp
editedOn?: Timestamp
reactions?: number
}
/**
@ -69,6 +70,16 @@ export interface Message extends ChunterMessage {
lastReply?: Timestamp
}
/**
* @public
*/
export interface Reaction extends AttachedDoc {
emoji: string
createBy: Ref<Account>
attachedTo: Ref<ChunterMessage>
attachedToClass: Ref<Class<ChunterMessage>>
}
/**
* @public
*/
@ -128,7 +139,8 @@ export default plugin(chunterId, {
ChunterSpace: '' as Ref<Class<ChunterSpace>>,
Channel: '' as Ref<Class<Channel>>,
SavedMessages: '' as Ref<Class<SavedMessages>>,
DirectMessage: '' as Ref<Class<DirectMessage>>
DirectMessage: '' as Ref<Class<DirectMessage>>,
Reaction: '' as Ref<Class<Reaction>>
},
space: {
Backlinks: '' as Ref<Space>