@taruvi/navkit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs['recommended-latest'],
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>taruvi-navkit</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/App.tsx"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@taruvi/navkit",
3
+ "version": "0.0.1",
4
+ "main": "src/App.tsx",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "peerDependencies": {
13
+ "@emotion/react": "^11.0.0",
14
+ "@emotion/styled": "^11.0.0",
15
+ "@fortawesome/fontawesome-svg-core": "^6.0.0",
16
+ "@fortawesome/free-regular-svg-icons": "^6.0.0",
17
+ "@fortawesome/free-solid-svg-icons": "^6.0.0",
18
+ "@fortawesome/react-fontawesome": "^0.2.0",
19
+ "@mui/material": "^5.0.0"
20
+ },
21
+ "dependencies": {
22
+ "@taruvi/sdk": "^1.0.7",
23
+ "react": "^19.1.1"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/js": "^9.36.0",
27
+ "@types/node": "^24.6.0",
28
+ "@types/react": "^19.1.16",
29
+ "@vitejs/plugin-react": "^5.0.4",
30
+ "babel-plugin-react-compiler": "^19.1.0-rc.3",
31
+ "eslint": "^9.36.0",
32
+ "eslint-plugin-react-hooks": "^5.2.0",
33
+ "eslint-plugin-react-refresh": "^0.4.22",
34
+ "globals": "^16.4.0",
35
+ "typescript": "~5.9.3",
36
+ "typescript-eslint": "^8.45.0",
37
+ "vite": "^7.1.7"
38
+ }
39
+ }
@@ -0,0 +1,65 @@
1
+ import { colours, dimensions } from './styles/variables'
2
+
3
+ export const appStyles = {
4
+ appBar: {
5
+ height: dimensions.navHeight,
6
+ },
7
+ toolbar: {
8
+ backgroundColor: colours.bg.white,
9
+ },
10
+ leftSection: {
11
+ display: 'flex',
12
+ flexGrow: 1,
13
+ alignItems: 'center',
14
+ justifyContent: 'start',
15
+ gap: '10px',
16
+ },
17
+ logo: {
18
+ maxHeight: '40px',
19
+ cursor: 'pointer',
20
+ },
21
+ defaultLogo: {
22
+ maxHeight: '22px',
23
+ cursor: 'pointer',
24
+ },
25
+ rightSection: {
26
+ display: 'flex',
27
+ gap: '10px',
28
+ alignItems: 'center',
29
+ },
30
+ modal: {
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ justifyContent: 'center',
34
+ },
35
+ chatCard: {
36
+ width: '90vw',
37
+ maxWidth: '1200px',
38
+ height: '80vh',
39
+ outline: 'none',
40
+ overflow: 'hidden',
41
+ position: 'relative' as const,
42
+ },
43
+ chatHeader: {
44
+ position: 'absolute' as const,
45
+ top: 0,
46
+ right: 0,
47
+ zIndex: 1400,
48
+ padding: '8px',
49
+ },
50
+ chatOpenButton: {
51
+ backgroundColor: colours.bg.white,
52
+ '&:hover': {
53
+ backgroundColor: colours.bg.light,
54
+ },
55
+ },
56
+ iconStyle: {
57
+ cursor: 'pointer',
58
+ fontSize: '20px',
59
+ },
60
+ backdrop: {
61
+ position: "absolute" as const,
62
+ inset: 0,
63
+ zIndex: 10,
64
+ },
65
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,111 @@
1
+ import AppBar from "@mui/material/AppBar"
2
+ import Toolbar from "@mui/material/Toolbar"
3
+ import IconButton from "@mui/material/IconButton"
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
5
+ import { useState } from "react"
6
+ import { AppLauncher, Profile, ProfileMenu, Shortcuts, ShortcutsMenu, MattermostChat } from "./components"
7
+ import { Box, Modal, Card } from "@mui/material"
8
+ import { appStyles } from "./App.styles"
9
+ import { NavkitProvider, useNavigation } from "./NavkitContext"
10
+ import defaultLogo from "./assets/taruvi-logo.png"
11
+
12
+ const NavkitContent = () => {
13
+ const { isUserAuthenticated, siteSettings, userData, appsList, jwtToken } = useNavigation()
14
+ const [showAppLauncher, setShowAppLauncher] = useState<boolean>(false)
15
+ const [showProfileMenu, setShowProfileMenu] = useState<boolean>(false)
16
+ const [showShortcutsMenu, setShowShortcutsMenu] = useState<boolean>(false)
17
+ const [showChat, setShowChat] = useState<boolean>(false)
18
+
19
+ return (
20
+ <>
21
+ {(showAppLauncher || showProfileMenu || showShortcutsMenu) && (
22
+ <div
23
+ style={appStyles.backdrop}
24
+ onClick={() => {
25
+ setShowAppLauncher(false)
26
+ setShowProfileMenu(false)
27
+ setShowShortcutsMenu(false)
28
+ }}
29
+ />
30
+ )}
31
+ <AppBar position="static" sx={appStyles.appBar}>
32
+ <Toolbar sx={appStyles.toolbar}>
33
+ <Box component={"div"} sx={appStyles.leftSection}>
34
+ {isUserAuthenticated &&
35
+ <IconButton onClick={() => setShowAppLauncher(!showAppLauncher)}>
36
+ <FontAwesomeIcon icon={["fas", "th"]} style={appStyles.iconStyle} />
37
+ </IconButton>
38
+ }
39
+ <img
40
+ style={(siteSettings?.logo && siteSettings.logo !== '') ? appStyles.logo : appStyles.defaultLogo}
41
+ src={(siteSettings?.logo && siteSettings.logo !== '') ? siteSettings.logo : defaultLogo}
42
+ onClick={() => window.location.href = siteSettings.frontendUrl || '/desk'}
43
+ />
44
+ </Box>
45
+ <Box component={'div'} sx={appStyles.rightSection}>
46
+ <Shortcuts
47
+ showChat={setShowChat}
48
+ onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)}
49
+ />
50
+ {isUserAuthenticated && userData && (
51
+ <Profile userData={userData} onClick={() => setShowProfileMenu(!showProfileMenu)} />
52
+ )}
53
+ </Box>
54
+ </Toolbar>
55
+ </AppBar>
56
+
57
+ {showAppLauncher && (
58
+ <Box>
59
+ <AppLauncher />
60
+ </Box>
61
+ )}
62
+
63
+ {showProfileMenu && (
64
+ <Box>
65
+ <ProfileMenu />
66
+ </Box>
67
+ )}
68
+
69
+ {showShortcutsMenu && (
70
+ <Box>
71
+ <ShortcutsMenu showChat={setShowChat} />
72
+ </Box>
73
+ )}
74
+
75
+ {showChat && siteSettings.mattermostUrl && (
76
+ <Modal
77
+ open={showChat}
78
+ onClose={() => setShowChat(false)}
79
+ sx={appStyles.modal}
80
+ >
81
+ <Card sx={appStyles.chatCard}>
82
+ <Box sx={appStyles.chatHeader}>
83
+ <IconButton
84
+ onClick={() => window.open(siteSettings.mattermostUrl, '_blank')}
85
+ sx={appStyles.chatOpenButton}
86
+ >
87
+ <FontAwesomeIcon icon={["fas", "external-link-alt"]} style={{ fontSize: '10px' }} />
88
+ </IconButton>
89
+ </Box>
90
+ <MattermostChat
91
+ jwtToken={"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3NjM1Mzk1NjUsImp0aSI6IjFyMEVBZnNJODNvYzg5Vjd4ellUbTVIU2wzZGdkcWRkVFNUdFMybXdzcUE9IiwibmJmIjoxNzYzNTM5NTY1LCJleHAiOjE3NjM2MTE1NjUsImRhdGEiOnsidXNlcm5hbWUiOiJjdXJyYW5jZG9kZGFiZWxlIiwiYWNjb3VudElkIjozLCJzZXNzaW9uSWQiOiI2ZjE4YTgwYy1iMWZiLTRkMzEtODI4My0yOTJhYmNmNzExZDEifSwiaXNzIjoiZW9zLXBsYXRmb3JtIiwiYXVkIjoiaHR0cHM6Ly90YXJ1dmkuZW94dmFudGFnZS5jb206NDQzIn0.XhOHyHj6LZixoh084IGdyb636hLsUereUDd2_oaeyotlHMZVelSS2OIJTurX3IPTsfpUyBGpsVv4OxthmSTBdg"}
92
+ mattermostUrl={siteSettings.mattermostUrl}
93
+ width="100%"
94
+ height="100%"
95
+ />
96
+ </Card>
97
+ </Modal>
98
+ )}
99
+ </>
100
+ )
101
+ }
102
+
103
+ const Navkit = ({ client }: { client: object }) => {
104
+ return (
105
+ <NavkitProvider client={client}>
106
+ <NavkitContent />
107
+ </NavkitProvider>
108
+ )
109
+ }
110
+
111
+ export default Navkit
@@ -0,0 +1,152 @@
1
+ import { createContext, startTransition, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
2
+ import type { AppData, NavigationContextType, NavkitProviderProps, SiteSettings, UserData } from './types'
3
+ import { User, Settings, Auth } from "@taruvi-io/sdk"
4
+ import { library } from '@fortawesome/fontawesome-svg-core'
5
+ import { fas } from '@fortawesome/free-solid-svg-icons'
6
+ import { far } from '@fortawesome/free-regular-svg-icons'
7
+
8
+ export const NavigationContext = createContext<NavigationContextType | undefined>(undefined)
9
+
10
+ export const NavkitProvider = ({ children, client }: NavkitProviderProps) => {
11
+ const auth = new Auth(client)
12
+
13
+ const user = useRef<any>(null)
14
+ const settings = useRef<any>(null)
15
+ const siteSettings = useRef<SiteSettings>({
16
+ shortcuts: [],
17
+ logo: '',
18
+ showChat: false,
19
+ frontendUrl: '',
20
+ mattermostUrl: ''
21
+ })
22
+
23
+ const [isDesk, setIsDesk] = useState<boolean>(false)
24
+ const [appsList, setAppsList] = useState<AppData[]>([])
25
+ const [userData, setUserData] = useState<UserData | null>(null)
26
+ const [jwtToken, setJwtToken] = useState<string>('')
27
+ const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean>(false)
28
+
29
+ const checkIfDesk = async () => {
30
+ const fetchedSettings = await settings.current.get()
31
+ const frontendUrl = fetchedSettings?.frontendUrl
32
+ // const frontendUrl = "http://localhost:5173/desk"
33
+ if (!frontendUrl) return false
34
+ return window.location.href.includes(frontendUrl)
35
+ }
36
+ const navigateToUrl = (url: string, openInLine: boolean) => {
37
+ if (isDesk && openInLine) {
38
+ try {
39
+ const urlObj = new URL(url)
40
+ window.location.href = urlObj.pathname
41
+ } catch {
42
+ window.location.href = url.startsWith('/') ? url : `/${url}`
43
+ }
44
+ } else {
45
+ window.open(url, "_blank")
46
+ }
47
+ }
48
+
49
+ const authenticateUser = async () => {
50
+ const authenticated = await auth.isUserAuthenticated()
51
+ setIsUserAuthenticated(authenticated)
52
+ getData(authenticated)
53
+
54
+ // Get JWT token if authenticated from sdk
55
+ if (authenticated) {
56
+ const token = ""
57
+ setJwtToken(token)
58
+ }
59
+ }
60
+
61
+ const getData = useCallback(async (isUserAuthenticated: boolean) => {
62
+ user.current = new User(client)
63
+ settings.current = new Settings(client)
64
+ const fetchedSettings = await settings?.current?.get()
65
+ // const fetchedSettings = {
66
+ // frontendUrl: "http://localhost:5173/",
67
+ // showChat: true,
68
+ // mattermostUrl: "https://chatprod.eoxvantage.com/",
69
+ // shortcuts: [
70
+ // {
71
+ // id: "shortcut-helpdesk",
72
+ // appname: "Helpdesk",
73
+ // icon: "comment-dots",
74
+ // url: "/helpdesk"
75
+ // },
76
+ // {
77
+ // id: "shortcut-crm",
78
+ // appname: "CRM",
79
+ // icon: "address-book",
80
+ // url: "/crm"
81
+ // },
82
+ // {
83
+ // id: "shortcut-tasks",
84
+ // appname: "Tasks",
85
+ // icon: "square-check",
86
+ // url: "/tasks"
87
+ // }
88
+ // ]
89
+ // }
90
+ siteSettings.current = fetchedSettings
91
+ if (isUserAuthenticated) {
92
+ const [apps, userDataResponse] = await Promise.all([
93
+ user.current.getApps?.(),
94
+ user.current.getUserData(),
95
+ ]);
96
+ startTransition(() => {
97
+ setAppsList(apps || [])
98
+ setUserData(userDataResponse || null)
99
+ })
100
+ }
101
+
102
+ }, [client])
103
+
104
+ useLayoutEffect(() => {
105
+ const init = async () => {
106
+ library.add(fas, far)
107
+ await authenticateUser()
108
+ }
109
+ init()
110
+ }, [])
111
+
112
+ useEffect(() => {
113
+ const initIsDesk = async () => {
114
+ const isDeskValue = await checkIfDesk()
115
+ setIsDesk(isDeskValue)
116
+ }
117
+
118
+ initIsDesk()
119
+ }, [])
120
+
121
+ // Listen for user profile updates via BroadcastChannel
122
+ useEffect(() => {
123
+ const channel = new BroadcastChannel('taruvi-updates')
124
+
125
+ const handleMessage = (event: MessageEvent) => {
126
+ if (event.data === 'refreshNavkit') {
127
+ getData(isUserAuthenticated)
128
+ }
129
+ }
130
+
131
+ channel.addEventListener('message', handleMessage)
132
+
133
+ return () => {
134
+ channel.removeEventListener('message', handleMessage)
135
+ channel.close()
136
+ }
137
+ }, [getData])
138
+
139
+ return (
140
+ <NavigationContext.Provider value={{ navigateToUrl, isDesk, appsList, userData, siteSettings: siteSettings.current, jwtToken, isUserAuthenticated }}>
141
+ {children}
142
+ </NavigationContext.Provider>
143
+ )
144
+ }
145
+
146
+ export const useNavigation = () => {
147
+ const context = useContext(NavigationContext)
148
+ if (context === undefined) {
149
+ throw new Error('useNavigation must be used within a NavkitProvider')
150
+ }
151
+ return context
152
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,59 @@
1
+ import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
2
+
3
+ export const appLauncherStyles = {
4
+ container: {
5
+ position: 'absolute' as const,
6
+ top: 60,
7
+ left: { xs: '2.5vw', sm: '10vw', md: '15vw', lg: 40 },
8
+ width: { xs: '95vw', sm: '80vw', md: '70vw', lg: '560px' },
9
+ maxWidth: '800px',
10
+ maxHeight: { xs: 'calc(100vh - 80px)', md: '335px' },
11
+ borderRadius: borderRadius.default,
12
+ boxShadow: shadows.dropdown,
13
+ p: spacing.lg,
14
+ backgroundColor: colours.bg.white,
15
+ zIndex: zIndex.overlay,
16
+ },
17
+ scrollContainer: {
18
+ mt: spacing.md,
19
+ maxHeight: { xs: '300px', md: '220px' },
20
+ overflowY: 'auto' as const,
21
+ },
22
+ appItem: {
23
+ display: 'flex',
24
+ flexDirection: 'column' as const,
25
+ alignItems: 'center',
26
+ justifyContent: 'center',
27
+ p: spacing.xs,
28
+ borderRadius: borderRadius.default,
29
+ cursor: 'pointer',
30
+ transition: 'all 0.2s ease',
31
+ '&:hover': {
32
+ backgroundColor: colours.bg.light,
33
+ },
34
+ },
35
+ iconContainer: {
36
+ width: 48,
37
+ height: 48,
38
+ display: 'flex',
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ borderRadius: borderRadius.default,
42
+ border: `1px solid ${colours.border.light}`,
43
+ mb: spacing.sm,
44
+ },
45
+ iconStyle: {
46
+ fontSize: dimensions.iconSize.md,
47
+ color: colours.text.secondary,
48
+ },
49
+ appName: {
50
+ fontWeight: typography.weights.regular,
51
+ color: colours.text.secondary,
52
+ fontSize: typography.sizes.xs,
53
+ textAlign: 'center' as const,
54
+ lineHeight: 1.3,
55
+ maxWidth: '100%',
56
+ wordWrap: 'break-word' as const,
57
+ letterSpacing: typography.letterSpacing.default,
58
+ },
59
+ }
@@ -0,0 +1,74 @@
1
+ import { useState, useEffect } from "react"
2
+ import type { AppData } from "../../types"
3
+ import Search from "../Search/Search"
4
+ import { Card, Box, Grid, Typography } from "@mui/material"
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
6
+ import type { IconName } from "@fortawesome/fontawesome-svg-core"
7
+ import { useNavigation } from "../../NavkitContext"
8
+ import { appLauncherStyles } from "./AppLauncher.styles"
9
+ import siteData from "../../../mocks/site.json"
10
+
11
+
12
+
13
+ const AppLauncher = () => {
14
+ const { navigateToUrl } = useNavigation()
15
+
16
+ // Transform apps from site.json to include id
17
+ const appsList: AppData[] = siteData.apps.map((app, index) => ({
18
+ ...app,
19
+ id: `app-${index}`
20
+ }))
21
+
22
+ const [filteredApps, setFilteredApps] = useState<AppData[]>(appsList)
23
+
24
+ const handleAppClick = (app: AppData) => {
25
+ navigateToUrl(app.url, false)
26
+ }
27
+
28
+ const handleSearchChange = (filtered: AppData[]) => {
29
+ setFilteredApps(filtered)
30
+ }
31
+
32
+ useEffect(() => {
33
+ setFilteredApps(appsList)
34
+ }, [])
35
+
36
+ return (
37
+ <Card sx={appLauncherStyles.container}>
38
+ <Search appsList={appsList} onSearchChange={handleSearchChange} />
39
+ {filteredApps && filteredApps.length > 0 && (
40
+ <Box sx={appLauncherStyles.scrollContainer}>
41
+ <Grid container spacing={1}>
42
+ {filteredApps.map((app) => {
43
+ return (
44
+ <Grid item xs={3} key={app.id}>
45
+ <Box
46
+ sx={appLauncherStyles.appItem}
47
+ onClick={() => handleAppClick(app)}
48
+ >
49
+ <Box
50
+ sx={appLauncherStyles.iconContainer}
51
+ >
52
+ <FontAwesomeIcon
53
+ icon={["far", app.icon.replace('fa-', '') as IconName]}
54
+ style={appLauncherStyles.iconStyle}
55
+ />
56
+ </Box>
57
+ <Typography
58
+ variant="body2"
59
+ sx={appLauncherStyles.appName}
60
+ >
61
+ {app.appname}
62
+ </Typography>
63
+ </Box>
64
+ </Grid>
65
+ )
66
+ })}
67
+ </Grid>
68
+ </Box>
69
+ )}
70
+ </Card>
71
+ )
72
+ }
73
+
74
+ export default AppLauncher
@@ -0,0 +1,132 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { Box, Badge } from '@mui/material'
3
+
4
+ interface MattermostChatProps {
5
+ jwtToken: string
6
+ mattermostUrl: string // Mattermost server URL (must be passed from parent app)
7
+ onNotification?: (count: number) => void
8
+ onUrlClick?: (url: string) => void
9
+ width?: string | number
10
+ height?: string | number
11
+ }
12
+
13
+ interface NotifyMessage {
14
+ event: string
15
+ data?: {
16
+ title?: string
17
+ body?: string
18
+ channel?: string
19
+ teamId?: string
20
+ url?: string
21
+ }
22
+ }
23
+
24
+ const MattermostChat = ({
25
+ jwtToken,
26
+ mattermostUrl,
27
+ onNotification,
28
+ onUrlClick,
29
+ width = '100%',
30
+ height = '100%'
31
+ }: MattermostChatProps) => {
32
+ const iframeRef = useRef<HTMLIFrameElement>(null)
33
+ const [unreadCount, setUnreadCount] = useState(0)
34
+ const [isWindowFocused, setIsWindowFocused] = useState(true)
35
+ useEffect(() => {
36
+ // Handle postMessage events from Mattermost iframe
37
+ const handleMessage = (event: MessageEvent<NotifyMessage>) => {
38
+ // Validate origin for security
39
+ const mattermostOrigin = new URL(mattermostUrl).origin
40
+ if (event.origin !== mattermostOrigin) {
41
+ console.warn('Received message from untrusted origin:', event.origin)
42
+ return
43
+ }
44
+
45
+ // Handle Notify events
46
+ if (event.data?.event === 'Notify') {
47
+ if (!isWindowFocused) {
48
+ setUnreadCount(prev => {
49
+ const newCount = prev + 1
50
+ onNotification?.(newCount)
51
+ return newCount
52
+ })
53
+ }
54
+
55
+ // Handle URL clicks if provided
56
+ if (event.data.data?.url && onUrlClick) {
57
+ onUrlClick(event.data.data.url)
58
+ }
59
+ }
60
+ }
61
+
62
+ // Handle window focus/blur to track unread messages
63
+ const handleFocus = () => {
64
+ setIsWindowFocused(true)
65
+ setUnreadCount(0)
66
+ onNotification?.(0)
67
+ }
68
+
69
+ const handleBlur = () => {
70
+ setIsWindowFocused(false)
71
+ }
72
+
73
+ // Add event listeners
74
+ window.addEventListener('message', handleMessage)
75
+ window.addEventListener('focus', handleFocus)
76
+ window.addEventListener('blur', handleBlur)
77
+
78
+ // Cleanup event listeners on unmount
79
+ return () => {
80
+ window.removeEventListener('message', handleMessage)
81
+ window.removeEventListener('focus', handleFocus)
82
+ window.removeEventListener('blur', handleBlur)
83
+ }
84
+ }, [mattermostUrl, isWindowFocused, onNotification, onUrlClick])
85
+
86
+ // Construct iframe URL with JWT authentication
87
+ const iframeUrl = `${mattermostUrl}/login?oxauth=${jwtToken}`
88
+
89
+ return (
90
+ <Box
91
+ sx={{
92
+ position: 'relative',
93
+ width,
94
+ height,
95
+ overflow: 'hidden'
96
+ }}
97
+ >
98
+ {unreadCount > 0 && (
99
+ <Badge
100
+ badgeContent={unreadCount}
101
+ color="error"
102
+ sx={{
103
+ position: 'absolute',
104
+ top: 16,
105
+ right: 16,
106
+ zIndex: 1000,
107
+ '& .MuiBadge-badge': {
108
+ fontSize: '1rem',
109
+ height: '28px',
110
+ minWidth: '28px',
111
+ borderRadius: '14px'
112
+ }
113
+ }}
114
+ />
115
+ )}
116
+ <iframe
117
+ ref={iframeRef}
118
+ src={iframeUrl}
119
+ title="Mattermost Chat"
120
+ allow="clipboard-read; clipboard-write"
121
+ style={{
122
+ width: '100%',
123
+ height: '100%',
124
+ border: 'none',
125
+ display: 'block'
126
+ }}
127
+ />
128
+ </Box>
129
+ )
130
+ }
131
+
132
+ export default MattermostChat