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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,13 +17,13 @@ import {
} from '@/services/messenger.service' } from '@/services/messenger.service'
import { messengerSignalR } from '@/services/messenger.signalr' import { messengerSignalR } from '@/services/messenger.signalr'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
import withHeaderItem from '@/utils/hoc/withHeaderItem'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import EmojiPicker, { EmojiClickData } from 'emoji-picker-react' 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 { import {
FaArrowLeft, FaArrowLeft,
FaCompress,
FaExpand,
FaPaperclip, FaPaperclip,
FaRegSmile, FaRegSmile,
FaSearch, FaSearch,
@ -33,6 +33,8 @@ import {
} from 'react-icons/fa' } from 'react-icons/fa'
import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5' import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5'
import { useLocalization } from '@/utils/hooks/useLocalization' 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 MAX_UPLOAD_SIZE = 10 * 1024 * 1024
const CONTACT_REFRESH_INTERVAL = 30000 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 auth = useStoreState((state) => state.auth)
const tenantId = auth.user.tenantId || auth.tenant?.tenantId const tenantId = auth.user.tenantId || auth.tenant?.tenantId
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const messageInputRef = useRef<HTMLTextAreaElement>(null) const messageInputRef = useRef<HTMLTextAreaElement>(null)
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
const { translate } = useLocalization() const { translate } = useLocalization()
const { textTheme } = useThemeClass()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [contacts, setContacts] = useState<MessengerContactDto[]>([]) const [contacts, setContacts] = useState<MessengerContactDto[]>([])
@ -83,7 +87,6 @@ const MessengerWidget = () => {
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [showEmoji, setShowEmoji] = useState(false) const [showEmoji, setShowEmoji] = useState(false)
const [multiSelect, setMultiSelect] = useState(false) const [multiSelect, setMultiSelect] = useState(false)
const [maximized, setMaximized] = useState(false)
const [unreadByContact, setUnreadByContact] = useState<Record<string, number>>({}) const [unreadByContact, setUnreadByContact] = useState<Record<string, number>>({})
const [reachableContactIds, setReachableContactIds] = useState<string[]>([]) const [reachableContactIds, setReachableContactIds] = useState<string[]>([])
@ -250,14 +253,24 @@ const MessengerWidget = () => {
}) })
} }
const closeWidget = () => { const closeWidget = useCallback(() => {
setOpen(false) setOpen(false)
setMaximized(false)
setSelectedIds([]) setSelectedIds([])
setText('') setText('')
setAttachments([]) setAttachments([])
setShowEmoji(false) 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) => { const handleFiles = async (files: FileList | null) => {
if (!files?.length) return if (!files?.length) return
@ -349,11 +362,10 @@ const MessengerWidget = () => {
setText((current) => `${current}${emoji.emoji}`) setText((current) => `${current}${emoji.emoji}`)
} }
if (!auth.session.signedIn) return null if (!auth.session.signedIn || !location.pathname.startsWith('/admin')) return null
const panelClassName = maximized 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' '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 headerActionClassName = 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' '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> </div>
<div className="flex shrink-0 items-center gap-1 sm:hidden"> <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 <Button
size="xs" size="xs"
variant="plain" variant="plain"
@ -570,18 +567,6 @@ const MessengerWidget = () => {
</div> </div>
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <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 <Button
size="xs" size="xs"
variant="plain" variant="plain"
@ -792,27 +777,42 @@ const MessengerWidget = () => {
</> </>
)} )}
<div className="fixed bottom-5 right-5 z-[52]"> <Tooltip title={translate('::MessengerWidget.Title')}>
<Tooltip title={translate('::MessengerWidget.Title')}> <div
<span className="pointer-events-auto relative inline-flex"> className={classNames(
<Button className,
type="button" textTheme,
size="lg" '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',
variant="solid" )}
shape="circle" aria-label={translate('::MessengerWidget.Title')}
icon={<IoChatbubbleEllipsesOutline />} role="button"
onClick={() => (open ? closeWidget() : setOpen(true))} tabIndex={0}
/> onClick={() => (open ? closeWidget() : setOpen(true))}
{unread > 0 && ( onKeyDown={(event) => {
<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"> if (event.key === 'Enter' || event.key === ' ') {
{unread > 99 ? '99+' : unread} event.preventDefault()
</span> if (open) {
)} closeWidget()
</span> } else {
</Tooltip> setOpen(true)
</div> }
}
}}
>
<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 export default MessengerWidget

View file

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

View file

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

View file

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