From 7abd2bedddb3fe5fcf3e905d39ecd93156353d39 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 13 May 2026 15:32:16 +0200 Subject: [PATCH 1/3] chore(ts): use declared types for models tests Signed-off-by: Maksim Sukharev --- src/utils/webrtc/LocalStateBroadcaster.spec.ts | 10 ++++++---- src/utils/webrtc/LocalStateBroadcasterMcu.spec.ts | 6 ++++-- src/utils/webrtc/LocalStateBroadcasterNoMcu.spec.ts | 6 ++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/utils/webrtc/LocalStateBroadcaster.spec.ts b/src/utils/webrtc/LocalStateBroadcaster.spec.ts index 7651c73aa3e..93128626bff 100644 --- a/src/utils/webrtc/LocalStateBroadcaster.spec.ts +++ b/src/utils/webrtc/LocalStateBroadcaster.spec.ts @@ -4,8 +4,10 @@ */ import type { + CallParticipantCollection as CallParticipantCollectionType, CallParticipantModel as CallParticipantModelType, InternalWebRtc, + LocalCallParticipantModel as LocalCallParticipantModelType, WebRtc, } from '../../types/index.ts' @@ -23,11 +25,11 @@ import { CallParticipantCollection } from './models/CallParticipantCollection.js import { LocalCallParticipantModel } from './models/LocalCallParticipantModel.js' class BaseLocalStateBroadcaster extends LocalStateBroadcaster { - protected _handleAddCallParticipantModel(callParticipantCollection: CallParticipantCollection, callParticipantModel: CallParticipantModelType): void { + protected _handleAddCallParticipantModel(callParticipantCollection: CallParticipantCollectionType, callParticipantModel: CallParticipantModelType): void { // Not used in base class tests } - protected _handleRemoveCallParticipantModel(callParticipantCollection: CallParticipantCollection, callParticipantModel: CallParticipantModelType): void { + protected _handleRemoveCallParticipantModel(callParticipantCollection: CallParticipantCollectionType, callParticipantModel: CallParticipantModelType): void { // Not used in base class tests } } @@ -35,8 +37,8 @@ class BaseLocalStateBroadcaster extends LocalStateBroadcaster { describe('LocalStateBroadcaster', () => { let webRtc: WebRtc let internalWebRtc: InternalWebRtc - let callParticipantCollection: CallParticipantCollection - let localCallParticipantModel: LocalCallParticipantModel + let callParticipantCollection: CallParticipantCollectionType + let localCallParticipantModel: LocalCallParticipantModelType let localStateBroadcaster: LocalStateBroadcaster diff --git a/src/utils/webrtc/LocalStateBroadcasterMcu.spec.ts b/src/utils/webrtc/LocalStateBroadcasterMcu.spec.ts index ddb3a5f2b46..30848b5a2ef 100644 --- a/src/utils/webrtc/LocalStateBroadcasterMcu.spec.ts +++ b/src/utils/webrtc/LocalStateBroadcasterMcu.spec.ts @@ -4,7 +4,9 @@ */ import type { + CallParticipantCollection as CallParticipantCollectionType, InternalWebRtc, + LocalCallParticipantModel as LocalCallParticipantModelType, WebRtc, } from '../../types/index.ts' @@ -24,8 +26,8 @@ import { LocalCallParticipantModel } from './models/LocalCallParticipantModel.js describe('LocalStateBroadcasterMcu', () => { let webRtc: WebRtc let internalWebRtc: InternalWebRtc - let callParticipantCollection: CallParticipantCollection - let localCallParticipantModel: LocalCallParticipantModel + let callParticipantCollection: CallParticipantCollectionType + let localCallParticipantModel: LocalCallParticipantModelType let localStateBroadcasterMcu: LocalStateBroadcasterMcu diff --git a/src/utils/webrtc/LocalStateBroadcasterNoMcu.spec.ts b/src/utils/webrtc/LocalStateBroadcasterNoMcu.spec.ts index b0d0b54fc82..ebf8d9294cd 100644 --- a/src/utils/webrtc/LocalStateBroadcasterNoMcu.spec.ts +++ b/src/utils/webrtc/LocalStateBroadcasterNoMcu.spec.ts @@ -4,7 +4,9 @@ */ import type { + CallParticipantCollection as CallParticipantCollectionType, InternalWebRtc, + LocalCallParticipantModel as LocalCallParticipantModelType, WebRtc, } from '../../types/index.ts' @@ -43,8 +45,8 @@ class PeerMock { describe('LocalStateBroadcasterNoMcu', () => { let webRtc: WebRtc let internalWebRtc: InternalWebRtc - let callParticipantCollection: CallParticipantCollection - let localCallParticipantModel: LocalCallParticipantModel + let callParticipantCollection: CallParticipantCollectionType + let localCallParticipantModel: LocalCallParticipantModelType let localStateBroadcasterNoMcu: LocalStateBroadcasterNoMcu From 917fea53733ea78099e3f4158932bb8b2ef5cfb7 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 13 May 2026 15:33:47 +0200 Subject: [PATCH 2/3] chore(ts): declare WebRTC models with individual d.ts files instead of env.d.ts augmentation - workaround before models are migrated to .ts themselves Signed-off-by: Maksim Sukharev --- src/env.d.ts | 31 ------ src/types/index.ts | 98 +++++++++++++------ .../models/CallParticipantCollection.d.ts | 8 ++ .../webrtc/models/CallParticipantModel.d.ts | 21 ++++ .../models/LocalCallParticipantModel.d.ts | 8 ++ src/utils/webrtc/models/LocalMediaModel.d.ts | 8 ++ 6 files changed, 113 insertions(+), 61 deletions(-) create mode 100644 src/utils/webrtc/models/CallParticipantCollection.d.ts create mode 100644 src/utils/webrtc/models/CallParticipantModel.d.ts create mode 100644 src/utils/webrtc/models/LocalCallParticipantModel.d.ts create mode 100644 src/utils/webrtc/models/LocalMediaModel.d.ts diff --git a/src/env.d.ts b/src/env.d.ts index 22704b43aa4..1a4fd45bec0 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -99,34 +99,3 @@ declare global { let __webpack_nonce__: ReturnType let __webpack_public_path__: string } - -// Augment models with the public methods added to their prototype by the -// EmitterMixin. -/* eslint-disable @typescript-eslint/no-explicit-any -- - * Arguments of function types are contravariant in strict mode, so the - * "any" type is required here, as the "unknown" type would prevent - * assigning a function type with narrower argument types. - */ -declare module './utils/webrtc/models/CallParticipantCollection.js' { - interface CallParticipantCollection { - on(event: string, handler: (callParticipantCollection: CallParticipantCollection, ...args: any[]) => void): void - off(event: string, handler: (callParticipantCollection: CallParticipantCollection, ...args: any[]) => void): void - } -} - -declare module './utils/webrtc/models/CallParticipantModel.js' { - interface CallParticipantModel { - on(event: string, handler: (callParticipantModel: CallParticipantModel, ...args: any[]) => void): void - off(event: string, handler: (callParticipantModel: CallParticipantModel, ...args: any[]) => void): void - } -} - -declare module './utils/webrtc/models/LocalCallParticipantModel.js' { - interface LocalCallParticipantModel { - on(event: string, handler: (localCallParticipantModel: LocalCallParticipantModel, ...args: any[]) => void): void - off(event: string, handler: (localCallParticipantModel: LocalCallParticipantModel, ...args: any[]) => void): void - } -} -/* eslint-enable @typescript-eslint/no-explicit-any */ - -export {} diff --git a/src/types/index.ts b/src/types/index.ts index 313aeb73b60..41181d92a72 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -245,17 +245,19 @@ export type JoinRoomFullResponse = { export type fetchPeersResponse = ApiResponse export type callSIPDialOutResponse = ApiResponse -export type CallParticipantCollection = { - callParticipantModels: Array +/* eslint-disable @typescript-eslint/no-explicit-any -- + * Arguments of function types are contravariant in strict mode, so the + * "any" type is required here, as the "unknown" type would prevent + * assigning a function type with narrower argument types. + */ +type SuperEmittedMixin = { + on(event: string, handler: (self: T, ...args: any[]) => void): void + off(event: string, handler: (self: T, ...args: any[]) => void): void +} +/* eslint-enable @typescript-eslint/no-explicit-any */ - /* eslint-disable @typescript-eslint/no-explicit-any -- - * Arguments of function types are contravariant in strict mode, so the - * "any" type is required here, as the "unknown" type would prevent - * assigning a function type with narrower argument types. - */ - on(event: string, handler: (callParticipantCollection: CallParticipantCollection, ...args: any[]) => void): void - off(event: string, handler: (callParticipantCollection: CallParticipantCollection, ...args: any[]) => void): void - /* eslint-enable @typescript-eslint/no-explicit-any */ +export interface CallParticipantCollection extends SuperEmittedMixin { + callParticipantModels: Array add(options: CallParticipantModelOptions): CallParticipantModel get(peerId: string): CallParticipantModel | undefined @@ -267,30 +269,24 @@ export type CallParticipantModelOptions = { webRtc: WebRtc } -export type CallParticipantModel = { - /* eslint-disable @typescript-eslint/no-explicit-any -- - * Arguments of function types are contravariant in strict mode, so the - * "any" type is required here, as the "unknown" type would prevent - * assigning a function type with narrower argument types. - */ - on(event: string, handler: (callParticipantModel: CallParticipantModel, ...args: any[]) => void): void - off(event: string, handler: (callParticipantModel: CallParticipantModel, ...args: any[]) => void): void - /* eslint-enable @typescript-eslint/no-explicit-any */ - +export interface CallParticipantModel extends SuperEmittedMixin { get(key: string): unknown set(key: string, value: unknown): void -} -export type LocalCallParticipantModel = { - /* eslint-disable @typescript-eslint/no-explicit-any -- - * Arguments of function types are contravariant in strict mode, so the - * "any" type is required here, as the "unknown" type would prevent - * assigning a function type with narrower argument types. - */ - on(event: string, handler: (localCallParticipantModel: LocalCallParticipantModel, ...args: any[]) => void): void - off(event: string, handler: (localCallParticipantModel: LocalCallParticipantModel, ...args: any[]) => void): void - /* eslint-enable @typescript-eslint/no-explicit-any */ + destroy(): void + forceMute(): void + setPeer(peer: unknown | null): void + setScreenPeer(screenPeer: unknown | null): void + setActor(actor: string | null): void + setUserId(userId: string): void + setNameForUserFromPeerNick(nick: string): void + setNextcloudSessionId(nextcloudSessionId: string): void + setVideoBlocked(blocked: boolean): void + setSimulcastVideoQuality(quality: number): void + setSimulcastScreenQuality(quality: number): void +} +export interface LocalCallParticipantModel extends SuperEmittedMixin { get(key: string): unknown set(key: string, value: unknown): void @@ -303,6 +299,48 @@ export type LocalCallParticipantModel = { sendReaction(reaction: string): void } +export interface LocalMediaModel extends SuperEmittedMixin { + attributes: { + localStream: unknown | null + audioAvailable: boolean + audioEnabled: boolean + speaking: boolean + speakingWhileMuted: boolean + currentVolume: number + volumeThreshold: number + videoAvailable: boolean + videoEnabled: boolean + virtualBackgroundAvailable: boolean + virtualBackgroundEnabled: boolean + virtualBackgroundType: string | null + virtualBackgroundBlurStrength: number | null + virtualBackgroundUrl: string | null + localScreen: unknown | null + token: string + raisedHand: { state: boolean, timestamp: Date } + } + + get(key: string): unknown + set(key: string, value: unknown): void + + getWebRtc(): WebRtc + setWebRtc(webRtc: WebRtc): void + enableAudio(): void + disableAudio(): void + enableVideo(): void + disableVideo(): void + enableNoiseSuppression(): void + disableNoiseSuppression(): void + enableVirtualBackground(): void + setVirtualBackgroundBlur(blurStrength: number, globalBlurVirtualBackground: boolean): void + setVirtualBackgroundImage(imageUrl: string): void + setVirtualBackgroundVideo(videoUrl: string): void + disableVirtualBackground(): void + shareScreen(mode: string | undefined, callback: () => void): void + stopSharingScreen(): void + toggleHandRaised(raised: boolean): void +} + export type Signaling = { settings: { userId: string | null diff --git a/src/utils/webrtc/models/CallParticipantCollection.d.ts b/src/utils/webrtc/models/CallParticipantCollection.d.ts new file mode 100644 index 00000000000..6d27a8dfab3 --- /dev/null +++ b/src/utils/webrtc/models/CallParticipantCollection.d.ts @@ -0,0 +1,8 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { CallParticipantCollection } from '../../../types/index.ts' + +export declare const CallParticipantCollection: new () => CallParticipantCollection diff --git a/src/utils/webrtc/models/CallParticipantModel.d.ts b/src/utils/webrtc/models/CallParticipantModel.d.ts new file mode 100644 index 00000000000..0ad8751a6ec --- /dev/null +++ b/src/utils/webrtc/models/CallParticipantModel.d.ts @@ -0,0 +1,21 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { CallParticipantModel, CallParticipantModelOptions } from '../../../types/index.ts' + +export declare const ConnectionState: { + NEW: 'new' + CHECKING: 'checking' + CONNECTED: 'connected' + COMPLETED: 'completed' + DISCONNECTED: 'disconnected' + DISCONNECTED_LONG: 'disconnected-long' + FAILED: 'failed' + FAILED_NO_RESTART: 'failed-no-restart' + CLOSED: 'closed' +} + +export declare const CallParticipantModel: new (options: CallParticipantModelOptions) => CallParticipantModel +export type { CallParticipantModelOptions } diff --git a/src/utils/webrtc/models/LocalCallParticipantModel.d.ts b/src/utils/webrtc/models/LocalCallParticipantModel.d.ts new file mode 100644 index 00000000000..7bfaf1ba40f --- /dev/null +++ b/src/utils/webrtc/models/LocalCallParticipantModel.d.ts @@ -0,0 +1,8 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { LocalCallParticipantModel } from '../../../types/index.ts' + +export declare const LocalCallParticipantModel: new () => LocalCallParticipantModel diff --git a/src/utils/webrtc/models/LocalMediaModel.d.ts b/src/utils/webrtc/models/LocalMediaModel.d.ts new file mode 100644 index 00000000000..459fbe96a5a --- /dev/null +++ b/src/utils/webrtc/models/LocalMediaModel.d.ts @@ -0,0 +1,8 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { LocalMediaModel } from '../../../types/index.ts' + +export declare const LocalMediaModel: new () => LocalMediaModel From cb2bb90ab53c5ffc7650e28f4073669911579c02 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Wed, 13 May 2026 14:00:27 +0200 Subject: [PATCH 3/3] feat: include 'Raise hand' in call reactions menu Signed-off-by: Maksim Sukharev --- src/components/CallView/BottomBar.vue | 132 +---------------------- src/components/TopBar/ReactionMenu.vue | 139 +++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 136 deletions(-) diff --git a/src/components/CallView/BottomBar.vue b/src/components/CallView/BottomBar.vue index 0caff13e4a3..631a8d9b83a 100644 --- a/src/components/CallView/BottomBar.vue +++ b/src/components/CallView/BottomBar.vue @@ -8,11 +8,10 @@ import { showError, } from '@nextcloud/dialogs' import { t } from '@nextcloud/l10n' -import { useHotKey } from '@nextcloud/vue/composables/useHotKey' import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile' import { useResizeObserver } from '@vueuse/core' import debounce from 'debounce' -import { computed, onMounted, onUnmounted, ref, toValue, useTemplateRef, watch } from 'vue' +import { computed, onMounted, onUnmounted, ref, toValue, useTemplateRef } from 'vue' import { useStore } from 'vuex' import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcActions from '@nextcloud/vue/components/NcActions' @@ -22,8 +21,6 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import IconChevronUp from 'vue-material-design-icons/ChevronUp.vue' import IconFullscreen from 'vue-material-design-icons/Fullscreen.vue' import IconFullscreenExit from 'vue-material-design-icons/FullscreenExit.vue' -import IconHandBackLeft from 'vue-material-design-icons/HandBackLeft.vue' // Filled for better indication -import IconHandBackLeftOutline from 'vue-material-design-icons/HandBackLeftOutline.vue' import IconSubtitles from 'vue-material-design-icons/Subtitles.vue' import IconSubtitlesOutline from 'vue-material-design-icons/SubtitlesOutline.vue' import IconViewGalleryOutline from 'vue-material-design-icons/ViewGalleryOutline.vue' @@ -36,13 +33,12 @@ import { useDocumentFullscreen, } from '../../composables/useDocumentFullscreen.ts' import { useGetToken } from '../../composables/useGetToken.ts' -import { ATTENDEE, CONVERSATION, PARTICIPANT } from '../../constants.ts' +import { ATTENDEE, CONVERSATION } from '../../constants.ts' import { getTalkConfig, showTalkFeatureHint, } from '../../services/CapabilitiesManager.ts' import { useActorStore } from '../../stores/actor.ts' -import { useBreakoutRoomsStore } from '../../stores/breakoutRooms.ts' import { useCallViewStore } from '../../stores/callView.ts' import { useLiveTranscriptionStore } from '../../stores/liveTranscription.ts' import { useSettingsStore } from '../../stores/settings.ts' @@ -51,13 +47,10 @@ import { localCallParticipantModel, localMediaModel } from '../../utils/webrtc/i const { isSidebar = false } = defineProps<{ isSidebar: boolean }>() -const AUTO_LOWER_HAND_THRESHOLD = 3000 -const disableKeyboardShortcuts = OCP.Accessibility.disableKeyboardShortcuts() const store = useStore() const token = useGetToken() const actorStore = useActorStore() -const breakoutRoomsStore = useBreakoutRoomsStore() const isFullscreen = !isSidebar && useDocumentFullscreen() const callViewStore = useCallViewStore() const liveTranscriptionStore = useLiveTranscriptionStore() @@ -74,13 +67,6 @@ const conversation = computed(() => { const isGuestActor = computed(() => actorStore.actorType === ATTENDEE.ACTOR_TYPE.GUESTS) const isVoiceRoom = computed(() => Boolean(conversation.value.attributes & CONVERSATION.ATTRIBUTE.VOICE_ROOM)) -const supportedReactions = computed(() => getTalkConfig(token.value, 'call', 'supported-reactions') || []) - -const hasReactionSupport = computed(() => supportedReactions.value && supportedReactions.value.length > 0) - -const canModerate = computed(() => [PARTICIPANT.TYPE.OWNER, PARTICIPANT.TYPE.MODERATOR, PARTICIPANT.TYPE.GUEST_MODERATOR] - .includes(conversation.value.participantType)) - const isLiveTranscriptionSupported = computed(() => getTalkConfig(token.value, 'call', 'live-transcription') || false) const hintTranscriptionSupported = computed(() => !isLiveTranscriptionSupported.value && showTalkFeatureHint(34)) const isLiveTranslationSupported = computed(() => getTalkConfig(token.value, 'call', 'live-translation') || false) @@ -164,19 +150,6 @@ const LanguageType = { const languageType = ref(LanguageType.Original) -const isHandRaised = computed(() => localMediaModel.attributes.raisedHand.state === true) - -const raiseHandButtonLabel = computed(() => { - if (!isHandRaised.value) { - return disableKeyboardShortcuts - ? t('spreed', 'Raise hand') - : t('spreed', 'Raise hand (R)') - } - return disableKeyboardShortcuts - ? t('spreed', 'Lower hand') - : t('spreed', 'Lower hand (R)') -}) - const fullscreenLabel = computed(() => { return toValue(isFullscreen) ? t('spreed', 'Exit full screen (F)') @@ -191,14 +164,12 @@ const changeViewLabel = computed(() => { const showCallLayoutSwitch = computed(() => !isSidebar && !callViewStore.isEmptyCallView) const isGrid = computed(() => callViewStore.isGrid) -const userIsInBreakoutRoomAndInCall = computed(() => conversation.value.objectType === CONVERSATION.OBJECT_TYPE.BREAKOUT_ROOM) -const COLLAPSIBLE_BUTTONS = ['virtualBackground', 'liveTranscription', 'raiseHand', 'callLayout', 'fullscreen'] as const +const COLLAPSIBLE_BUTTONS = ['virtualBackground', 'liveTranscription', 'callLayout', 'fullscreen'] as const type CollapsibleButtons = Record const isActionAvailableMask = computed(() => ({ fullscreen: !isSidebar, callLayout: showCallLayoutSwitch.value, - raiseHand: !isSidebar, liveTranscription: !isSidebar && isLiveTranscriptionSupported.value, virtualBackground: !isSidebar, })) @@ -448,68 +419,6 @@ async function disableLiveTranslation() { languageType.value = LanguageType.Original } -let lowerHandDelay = AUTO_LOWER_HAND_THRESHOLD -let speakingTimestamp: number | null = null -let lowerHandTimeout: ReturnType | null = null - -// Hand raising functionality -/** - * Toggle the hand raised state for the local media model and update the store. - * If the user is in a breakout room, it also handles the request for assistance. - */ -function toggleHandRaised() { - const newState = !isHandRaised.value - localMediaModel.toggleHandRaised(newState) - store.dispatch('setParticipantHandRaised', { - sessionId: actorStore.sessionId, - raisedHand: localMediaModel.attributes.raisedHand, - }) - - // Handle breakout room assistance requests - if (userIsInBreakoutRoomAndInCall.value && !canModerate.value) { - const hasRaisedHands = Object.keys(store.getters.participantRaisedHandList) - .filter((sessionId) => sessionId !== actorStore.sessionId) - .length !== 0 - - if (hasRaisedHands) { - return // Assistance is already requested by someone in the room - } - - const hasAssistanceRequested = conversation.value.breakoutRoomStatus === CONVERSATION.BREAKOUT_ROOM_STATUS.STATUS_ASSISTANCE_REQUESTED - if (newState && !hasAssistanceRequested) { - breakoutRoomsStore.requestAssistance(token.value) - } else if (!newState && hasAssistanceRequested) { - breakoutRoomsStore.dismissRequestAssistance(token.value) - } - } -} - -// Auto-lower hand when speaking -watch(() => localMediaModel.attributes.speaking, (speaking) => { - if (lowerHandTimeout !== null && !speaking) { - lowerHandDelay = Math.max(0, lowerHandDelay - (Date.now() - speakingTimestamp!)) - clearTimeout(lowerHandTimeout) - lowerHandTimeout = null - return - } - - // User is not speaking OR timeout is already running OR hand is not raised - if (!speaking || lowerHandTimeout !== null || !isHandRaised.value) { - return - } - - speakingTimestamp = Date.now() - lowerHandTimeout = setTimeout(() => { - lowerHandTimeout = null - speakingTimestamp = null - lowerHandDelay = AUTO_LOWER_HAND_THRESHOLD - - if (isHandRaised.value) { - toggleHandRaised() - } - }, lowerHandDelay) -}) - /** * Switches the call view mode between grid and speaker view. */ @@ -517,9 +426,6 @@ function changeView() { callViewStore.setCallViewMode({ token: token.value, isGrid: !isGrid.value, clearLast: false }) callViewStore.setSelectedVideoPeerId(null) } - -// Keyboard shortcuts -useHotKey('r', toggleHandRaised) {{ liveTranslationButtonLabel }} - - - - {{ raiseHandButtonLabel }} - @@ -95,7 +212,7 @@ function sendReaction(reaction: string) { flex-wrap: wrap; justify-content: flex-start; gap: 0 !important; - width: calc(var(--reactions-in-single-row) * var(--default-clickable-area)) + min-width: calc(var(--reactions-in-single-row) * var(--default-clickable-area)) } } @@ -103,4 +220,8 @@ function sendReaction(reaction: string) { flex: 0 0 calc(100% / var(--reactions-in-single-row)) !important; } } + +.raise-hand__button :deep(.action-button) { + justify-content: center; +}