Messenger Widget güncellemesi

This commit is contained in:
Sedat Öztürk 2026-07-04 22:36:50 +03:00
parent 8118404895
commit e96498645a
10 changed files with 75 additions and 65 deletions

View file

@ -7,6 +7,7 @@ import SidePanel from '@/components/template/SidePanel'
import UserDropdown from '@/components/template/UserDropdown'
import { useStoreState } from '@/store'
import LanguageSelector from '../template/LanguageSelector'
import MessengerWidget from '../template/MessengerWidget'
import Search from '../template/Search'
import StackedSideNav from '../template/StackedSideNav'
import AiAssistant from '../template/AiAssistant'
@ -27,6 +28,7 @@ const HeaderActionsEnd = () => {
<>
<AiAssistant />
<Notification />
<MessengerWidget />
<LanguageSelector />
<SidePanel className="hidden" />
<UserDropdown hoverable={false} />

View file

@ -8,6 +8,7 @@ import MobileNav from '@/components/template/MobileNav'
import View from '@/views/Views'
import Search from '@/components/template/Search'
import LanguageSelector from '../template/LanguageSelector'
import MessengerWidget from '../template/MessengerWidget'
import AiAssistant from '../template/AiAssistant'
const HeaderActionsStart = () => {
@ -25,6 +26,7 @@ const HeaderActionsEnd = () => {
<AiAssistant />
<Search />
<Notification />
<MessengerWidget />
<LanguageSelector />
<SidePanel className="hidden" />
<UserDropdown hoverable={false} />

View file

@ -15,7 +15,6 @@ import useLocale from '@/utils/hooks/useLocale'
import { useDynamicRoutes } from '@/routes/dynamicRoutesContext'
import { useLocation } from 'react-router-dom'
import { hasSubdomain } from '@/utils/subdomain'
import MessengerWidget from '../template/MessengerWidget'
export type LayoutType =
| typeof LAYOUT_TYPE_CLASSIC
@ -103,7 +102,6 @@ const Layout = () => {
}
>
<AppLayout />
{authenticated && currentPath.includes('/admin') && <MessengerWidget />}
</Suspense>
)
}

View file

@ -8,6 +8,7 @@ import SidePanel from '@/components/template/SidePanel'
import UserDropdown from '@/components/template/UserDropdown'
import View from '@/views/Views'
import LanguageSelector from '../template/LanguageSelector'
import MessengerWidget from '../template/MessengerWidget'
import { useStoreState } from '@/store'
import StackedSideNav from '../template/StackedSideNav'
import AiAssistant from '../template/AiAssistant'
@ -27,6 +28,7 @@ const HeaderActionsEnd = () => {
<>
<AiAssistant />
<Notification />
<MessengerWidget />
<LanguageSelector />
<SidePanel className="hidden" />
<UserDropdown hoverable={false} />

View file

@ -8,6 +8,7 @@ import View from '@/views/Views'
import Notification from '@/components/template/Notification'
import Search from '@/components/template/Search'
import LanguageSelector from '../template/LanguageSelector'
import MessengerWidget from '../template/MessengerWidget'
import AiAssistant from '../template/AiAssistant'
const HeaderActionsStart = () => {
@ -25,6 +26,7 @@ const HeaderActionsEnd = () => {
<Search />
<AiAssistant />
<Notification />
<MessengerWidget />
<LanguageSelector />
<SidePanel className="hidden" />
<UserDropdown hoverable={false} />

View file

@ -7,6 +7,7 @@ import View from '@/views/Views'
import Search from '@/components/template/Search'
import Notification from '@/components/template/Notification'
import LanguageSelector from '../template/LanguageSelector'
import MessengerWidget from '../template/MessengerWidget'
import AiAssistant from '../template/AiAssistant'
const HeaderActionsStart = () => {
@ -23,6 +24,7 @@ const HeaderActionsEnd = () => {
<>
<AiAssistant />
<Notification />
<MessengerWidget />
<LanguageSelector />
<SidePanel className="hidden" />
<UserDropdown hoverable={false} />

View file

@ -17,13 +17,13 @@ import {
} from '@/services/messenger.service'
import { messengerSignalR } from '@/services/messenger.signalr'
import { useStoreState } from '@/store'
import withHeaderItem from '@/utils/hoc/withHeaderItem'
import dayjs from 'dayjs'
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import classNames from 'classnames'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
FaArrowLeft,
FaCompress,
FaExpand,
FaPaperclip,
FaRegSmile,
FaSearch,
@ -33,6 +33,8 @@ import {
} from 'react-icons/fa'
import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5'
import { useLocalization } from '@/utils/hooks/useLocalization'
import useThemeClass from '@/utils/hooks/useThemeClass'
import { useLocation } from 'react-router-dom'
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024
const CONTACT_REFRESH_INTERVAL = 30000
@ -64,13 +66,15 @@ const mergeMessages = (
)
}
const MessengerWidget = () => {
const MessengerWidgetContent = ({ className }: { className?: string }) => {
const location = useLocation()
const auth = useStoreState((state) => state.auth)
const tenantId = auth.user.tenantId || auth.tenant?.tenantId
const fileInputRef = useRef<HTMLInputElement>(null)
const messageInputRef = useRef<HTMLTextAreaElement>(null)
const messagesEndRef = useRef<HTMLDivElement>(null)
const { translate } = useLocalization()
const { textTheme } = useThemeClass()
const [open, setOpen] = useState(false)
const [contacts, setContacts] = useState<MessengerContactDto[]>([])
@ -83,7 +87,6 @@ const MessengerWidget = () => {
const [uploading, setUploading] = useState(false)
const [showEmoji, setShowEmoji] = useState(false)
const [multiSelect, setMultiSelect] = useState(false)
const [maximized, setMaximized] = useState(false)
const [unreadByContact, setUnreadByContact] = useState<Record<string, number>>({})
const [reachableContactIds, setReachableContactIds] = useState<string[]>([])
@ -250,14 +253,24 @@ const MessengerWidget = () => {
})
}
const closeWidget = () => {
const closeWidget = useCallback(() => {
setOpen(false)
setMaximized(false)
setSelectedIds([])
setText('')
setAttachments([])
setShowEmoji(false)
}
}, [])
useEffect(() => {
if (!open) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') closeWidget()
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [closeWidget, open])
const handleFiles = async (files: FileList | null) => {
if (!files?.length) return
@ -349,11 +362,10 @@ const MessengerWidget = () => {
setText((current) => `${current}${emoji.emoji}`)
}
if (!auth.session.signedIn) return null
if (!auth.session.signedIn || !location.pathname.startsWith('/admin')) return null
const panelClassName = maximized
? 'pointer-events-auto fixed inset-2 z-[51] overflow-hidden rounded-xl border border-gray-200 bg-white/80 shadow-[0_24px_80px_rgba(15,23,42,0.35)] backdrop-blur-xl dark:border-gray-700 dark:bg-gray-900/85 sm:inset-4'
: 'pointer-events-auto fixed inset-x-2 bottom-20 top-3 z-[51] overflow-hidden rounded-xl border border-gray-200 bg-white/80 shadow-[0_24px_80px_rgba(15,23,42,0.35)] backdrop-blur-xl dark:border-gray-700 dark:bg-gray-900/85 sm:inset-auto sm:bottom-20 sm:right-5 sm:h-[min(75vh,680px)] sm:w-[min(78vw,800px)]'
const panelClassName =
'pointer-events-auto fixed inset-2 z-[51] overflow-hidden rounded-xl border border-gray-200 bg-white/80 shadow-[0_24px_80px_rgba(15,23,42,0.35)] backdrop-blur-xl dark:border-gray-700 dark:bg-gray-900/85 sm:inset-4'
const headerActionClassName =
'text-gray-600 hover:bg-gray-900/10 hover:text-gray-900 active:bg-gray-900/15 dark:text-gray-200 dark:hover:bg-white/10 dark:hover:text-white dark:active:bg-white/15'
@ -405,21 +417,6 @@ const MessengerWidget = () => {
</div>
</div>
<div className="flex shrink-0 items-center gap-1 sm:hidden">
<Tooltip
title={
maximized
? translate('::MessengerWidget.Minimize')
: translate('::MessengerWidget.Maximize')
}
>
<Button
size="xs"
variant="plain"
icon={maximized ? <FaCompress /> : <FaExpand />}
className={headerActionClassName}
onClick={() => setMaximized((value) => !value)}
/>
</Tooltip>
<Button
size="xs"
variant="plain"
@ -570,18 +567,6 @@ const MessengerWidget = () => {
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button
size="xs"
variant="plain"
icon={maximized ? <FaCompress /> : <FaExpand />}
className={headerActionClassName}
onClick={() => setMaximized((value) => !value)}
title={
maximized
? translate('::MessengerWidget.Minimize')
: translate('::MessengerWidget.Maximize')
}
/>
<Button
size="xs"
variant="plain"
@ -792,27 +777,42 @@ const MessengerWidget = () => {
</>
)}
<div className="fixed bottom-5 right-5 z-[52]">
<Tooltip title={translate('::MessengerWidget.Title')}>
<span className="pointer-events-auto relative inline-flex">
<Button
type="button"
size="lg"
variant="solid"
shape="circle"
icon={<IoChatbubbleEllipsesOutline />}
onClick={() => (open ? closeWidget() : setOpen(true))}
/>
{unread > 0 && (
<span className="absolute -right-1 -top-1 flex min-h-[20px] min-w-[20px] items-center justify-center rounded-full bg-red-500 px-1.5 text-xs font-semibold leading-none text-white ring-2 ring-white dark:ring-gray-800">
{unread > 99 ? '99+' : unread}
</span>
)}
</span>
</Tooltip>
</div>
<Tooltip title={translate('::MessengerWidget.Title')}>
<div
className={classNames(
className,
textTheme,
'pointer-events-auto relative !mx-0 text-2xl transition-colors duration-300 ease-in-out hover:bg-black hover:bg-opacity-5 dark:hover:bg-black dark:hover:bg-opacity-40',
)}
aria-label={translate('::MessengerWidget.Title')}
role="button"
tabIndex={0}
onClick={() => (open ? closeWidget() : setOpen(true))}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
if (open) {
closeWidget()
} else {
setOpen(true)
}
}
}}
>
<IoChatbubbleEllipsesOutline />
{unread > 0 && (
<span className="absolute -right-1 -top-1 flex min-h-[15px] min-w-[15px] items-center justify-center rounded-full bg-red-500 px-1.5 text-xs font-semibold leading-none text-white ring-2 ring-white dark:ring-gray-800">
{unread > 99 ? '99+' : unread}
</span>
)}
</div>
</Tooltip>
</>
)
}
const MessengerWidgetHeaderItem = withHeaderItem(MessengerWidgetContent)
const MessengerWidget = () => <MessengerWidgetHeaderItem hoverable={false} />
export default MessengerWidget

View file

@ -111,12 +111,12 @@ const StyleSwitcher = ({ onStyleChange }: { onStyleChange?: () => void }) => {
return (
<Select
menuPlacement="top"
components={{
Option: CustomSelectOption,
Control: CustomControl,
}}
isSearchable={true}
maxMenuHeight={menuItemHeight * 7}
options={styleMapOptions}
value={styleMapOptions.find((o) => o.value === style)}
onChange={(option: any) => {

View file

@ -27,14 +27,14 @@ const ThemeConfigurator = ({ callBackClose }: ThemeConfiguratorProps) => {
<h6 className="mb-3">{translate('::SidePanel.Layout')}</h6>
<LayoutSwitcher />
</div>
<div>
<h6 className="mb-3">{translate('::App.SiteManagement.Theme.Style')}</h6>
<StyleSwitcher onStyleChange={handleStyleChange} />
</div>
<div>
<h6 className="mb-3">{translate('::SidePanel.NavMode')}</h6>
<NavModeSwitcher />
</div>
<div>
<h6 className="mb-3">{translate('::App.SiteManagement.Theme.Style')}</h6>
<StyleSwitcher onStyleChange={handleStyleChange} />
</div>
<div>
<h6 className="mb-3">{translate('::SidePanel.Themed')}</h6>
<ThemeSwitcher />

View file

@ -104,6 +104,7 @@ const ThemeSwitcher = () => {
return (
<div className="grid grid-cols-2 gap-4">
<Select<ColorList>
menuPlacement="top"
size="sm"
options={colorList}
components={{
@ -114,6 +115,7 @@ const ThemeSwitcher = () => {
onChange={(opt) => onThemeColorChange(opt as ColorList)}
/>
<Select<ColorLevelList>
menuPlacement="top"
size="sm"
options={colorLevelList}
value={colorLevelList.filter((color) => color.value === primaryColorLevel)}