@taruvi/navkit 0.0.48-beta.8 → 0.0.48

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taruvi/navkit",
3
- "version": "0.0.48-beta.8",
3
+ "version": "0.0.48",
4
4
  "main": "src/App.tsx",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -48,6 +48,6 @@
48
48
  "vitest": "^4.0.18"
49
49
  },
50
50
  "dependencies": {
51
- "@taruvi/sdk": "beta"
51
+ "@taruvi/sdk": "latest"
52
52
  }
53
53
  }
package/src/App.styles.ts CHANGED
@@ -53,9 +53,9 @@ export const appStyles = {
53
53
  padding: '8px',
54
54
  },
55
55
  chatOpenButton: {
56
- backgroundColor: 'transparent',
56
+ backgroundColor: colours.bg.white,
57
57
  '&:hover': {
58
- backgroundColor: 'transparent',
58
+ backgroundColor: colours.bg.light,
59
59
  },
60
60
  },
61
61
  iconStyle: {
@@ -139,9 +139,9 @@ export const getAppStyles = (mode: ThemeMode) => {
139
139
  padding: '8px',
140
140
  },
141
141
  chatOpenButton: {
142
- backgroundColor: 'transparent',
142
+ backgroundColor: colors.bg.white,
143
143
  '&:hover': {
144
- backgroundColor: 'transparent',
144
+ backgroundColor: colors.bg.light,
145
145
  },
146
146
  },
147
147
  iconStyle: {
package/src/App.tsx CHANGED
@@ -1,7 +1,6 @@
1
1
  import AppBar from "@mui/material/AppBar"
2
2
  import Toolbar from "@mui/material/Toolbar"
3
3
  import IconButton from "@mui/material/IconButton"
4
- import { SvgIcon } from "@mui/material"
5
4
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
6
5
  import { useState } from "react"
7
6
  import { AppLauncher, Profile, ProfileMenu, Shortcuts, ShortcutsMenu, MattermostChat } from "./components"
@@ -13,31 +12,8 @@ import taruviLogo from "./assets/logo.svg";
13
12
  import taruviLogoWhite from "./assets/taruvi-logo-white.png";
14
13
  import type { Client } from "@taruvi/sdk"
15
14
 
16
- /** Top-level storage access for the chat origin (Chromium). Call synchronously from the chat button click
17
- * so transient activation is preserved; pairs with the iframe autologin bridge so no second in-frame login
18
- * click is needed when the browser supports this API. */
19
- function requestMattermostStorageAccessFromUserGesture(chatUrl: string) {
20
- try {
21
- const origin = new URL(chatUrl).origin
22
- const doc = document as Document & {
23
- requestStorageAccessFor?: (requestedOrigin: string) => Promise<void>
24
- }
25
- if (typeof doc.requestStorageAccessFor === "function") {
26
- void doc.requestStorageAccessFor(origin).catch(() => {})
27
- }
28
- } catch {
29
- // invalid chatUrl or API unavailable
30
- }
31
- }
32
-
33
- const ChatIcon = ({ color }: { color?: string }) => (
34
- <SvgIcon viewBox="-2 -2 24 24" sx={{ fontSize: 23, display: 'block', position: 'relative', top: 2 }}>
35
- <path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0Z" fill={color || "#9DE5FD"} />
36
- </SvgIcon>
37
- )
38
-
39
15
  const NavkitContent = () => {
40
- const { isUserAuthenticated, appSettings, appSettingsLoaded, appName, userData, appsList, chatUrl, themeMode, navbarColor, iconColor, sessionToken } = useNavigation();
16
+ const { isUserAuthenticated, siteSettings, appSettings, appSettingsLoaded, appName, userData, jwtToken, themeMode, navbarColor, iconColor } = useNavigation();
41
17
  const resolvedAppName = appName || appSettings?.displayName
42
18
  const showTaruviLogo = appSettingsLoaded && !resolvedAppName && !appSettings?.icon
43
19
  const styles = getAppStyles(themeMode)
@@ -76,18 +52,7 @@ const NavkitContent = () => {
76
52
  ) : null}
77
53
  </Box>
78
54
  <Box component={"div"} sx={styles.rightSection}>
79
- <Shortcuts onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)} iconColor={iconColor} />
80
- {isUserAuthenticated && appsList.some(a => a.id === 'chat') && chatUrl && (
81
- <Box
82
- onClick={() => {
83
- requestMattermostStorageAccessFromUserGesture(chatUrl)
84
- setShowChat(true)
85
- }}
86
- sx={{ ...styles.appLauncherContainer, cursor: 'pointer' }}
87
- >
88
- <ChatIcon color={iconColor} />
89
- </Box>
90
- )}
55
+ <Shortcuts showChat={setShowChat} onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)} iconColor={iconColor} />
91
56
  <Box
92
57
  onClick={() => {
93
58
  if (isUserAuthenticated) {
@@ -134,28 +99,18 @@ const NavkitContent = () => {
134
99
 
135
100
  {showShortcutsMenu && (
136
101
  <Box>
137
- <ShortcutsMenu />
102
+ <ShortcutsMenu showChat={setShowChat} />
138
103
  </Box>
139
104
  )}
140
105
 
141
- {showChat && chatUrl && (
142
- <DraggableResizable
143
- onClose={() => setShowChat(false)}
144
- initialWidth={Math.min(1200, window.innerWidth * 0.9)}
145
- initialHeight={Math.min(600, window.innerHeight * 0.8)}
146
- titleBarActions={
147
- <IconButton onClick={() => window.open(chatUrl, "_blank")} sx={styles.chatOpenButton}>
106
+ {showChat && siteSettings['chat-url'] && (
107
+ <DraggableResizable onClose={() => setShowChat(false)} initialWidth={Math.min(1200, window.innerWidth * 0.9)} initialHeight={Math.min(600, window.innerHeight * 0.8)}>
108
+ <Box sx={styles.chatHeader}>
109
+ <IconButton onClick={() => window.open(siteSettings['chat-url'], "_blank")} sx={styles.chatOpenButton}>
148
110
  <FontAwesomeIcon icon={["fas", "external-link-alt"]} style={{ fontSize: "10px" }} />
149
111
  </IconButton>
150
- }
151
- >
152
- <MattermostChat
153
- mattermostUrl={chatUrl}
154
- loginId={userData?.username ?? ''}
155
- sessionToken={sessionToken ?? ''}
156
- width="100%"
157
- height="100%"
158
- />
112
+ </Box>
113
+ <MattermostChat jwtToken={jwtToken} mattermostUrl={siteSettings['chat-url']} loginId={userData?.email || ''} width="100%" height="100%" />
159
114
  </DraggableResizable>
160
115
  )}
161
116
  </>
@@ -2,7 +2,7 @@ import { version } from '../package.json' with { type: 'json' }
2
2
  import { createContext, startTransition, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
3
3
  import type { AppData, AppSettings, NavigationContextType, NavkitProviderProps, SiteSettings, UserData } from './types'
4
4
  import type { ThemeMode } from './styles/variables'
5
- import { User, Settings, Auth, App, Secrets } from "@taruvi/sdk"
5
+ import { User, Settings, Auth, App } from "@taruvi/sdk"
6
6
  import { library } from '@fortawesome/fontawesome-svg-core'
7
7
  import { fas } from '@fortawesome/free-solid-svg-icons'
8
8
  import { far } from '@fortawesome/free-regular-svg-icons'
@@ -24,7 +24,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
24
24
 
25
25
  const user = useRef<any>(null)
26
26
  const settings = useRef<any>(null)
27
- const [siteSettings, setSiteSettings] = useState<SiteSettings>({
27
+ const siteSettings = useRef<SiteSettings>({
28
28
  shortcuts: [],
29
29
  logo: '',
30
30
  'show-chat': false,
@@ -38,8 +38,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
38
38
  const [isDesk, setIsDesk] = useState<boolean>(false)
39
39
  const [appsList, setAppsList] = useState<AppData[]>([])
40
40
  const [userData, setUserData] = useState<UserData | null>(null)
41
- const [chatUrl, setChatUrl] = useState<string>('')
42
- const [sessionToken, setSessionToken] = useState<string>('')
41
+ const [jwtToken, setJwtToken] = useState<string>('')
43
42
  const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean>(false)
44
43
  const [themeMode, setThemeMode] = useState<ThemeMode>(getInitialTheme)
45
44
 
@@ -76,8 +75,8 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
76
75
 
77
76
  // Get JWT token if authenticated from sdk
78
77
  if (authenticated) {
79
- const token = auth.getSessionToken() || ''
80
- setSessionToken(token)
78
+ const token = client.tokenClient.getToken() || ''
79
+ setJwtToken(token)
81
80
  }
82
81
  }
83
82
 
@@ -105,48 +104,45 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
105
104
  }
106
105
 
107
106
  const rawSettings = (fetchedSettings?.data ?? fetchedSettings)?.settings ?? {}
108
-
109
- let chatUrl = ''
110
- try {
111
- const secretResponse = await new Secrets(client).get('chat-url').execute<{ data: { value: string | { chat_url?: string, url?: string } } }>()
112
- const secretValue = secretResponse?.data?.value
113
- chatUrl = typeof secretValue === 'string'
114
- ? secretValue
115
- : (secretValue?.chat_url ?? secretValue?.url ?? '')
116
- } catch { }
117
- setChatUrl(chatUrl)
118
-
119
- const newSiteSettings: SiteSettings = {
120
- shortcuts: [],
121
- logo: rawSettings['navkit.logo'] ?? '',
107
+ siteSettings.current = {
108
+ ...siteSettings.current,
122
109
  'show-chat': rawSettings['navkit.show-chat'] ?? false,
123
- 'chat-url': chatUrl,
110
+ 'chat-url': rawSettings['navkit.chat-url'] ?? '',
124
111
  frontendUrl: rawSettings['navkit.frontend-url'] ?? '',
112
+ logo: rawSettings['navkit.logo'] ?? '',
125
113
  enableDarkMode: rawSettings['navkit.enable-dark-mode'] ?? false,
126
114
  }
127
- setSiteSettings(newSiteSettings)
128
115
  if (isUserAuthenticated) {
129
116
  const userDataResponse = await auth.getCurrentUser()
130
117
  setUserData(userDataResponse?.data || null)
131
118
 
132
119
  // Fetch user apps using username from userData
133
120
  if (userDataResponse?.data?.username) {
134
- const appsResponse = await user.current.getUserApps?.(userDataResponse.data.username)
135
- // Transform API response to match AppData interface
136
- const transformedApps: AppData[] = (appsResponse?.data || []).map?.((app: any) => ({
137
- id: app.slug,
138
- appname: app.display_name || app.name,
139
- icon: app.icon || "",
140
- url: app.url
141
- }))
142
- startTransition(() => {
143
- setAppsList(transformedApps)
144
- })
121
+ fetchUserAppsList(userDataResponse.data.username)
145
122
  }
146
123
  }
147
124
 
148
125
  }, [appName, client])
149
126
 
127
+ const fetchUserAppsList = async (name?: string) => {
128
+ // Prefer the explicitly passed username (used on first load, when userData
129
+ // state hasn't committed yet); fall back to committed userData for the
130
+ // argument-less onError refresh path.
131
+ const username = name ?? userData?.username
132
+ // Transform API response to match AppData interface
133
+ const appsResponse = await user.current?.getUserApps?.(username)
134
+ const transformedApps: AppData[] = (appsResponse?.data || []).map?.((app: any) => ({
135
+ id: app.slug,
136
+ appname: app.display_name || app.name,
137
+ icon: app.icon || "",
138
+ url: app.url
139
+ }))
140
+ startTransition(() => {
141
+ setAppsList(transformedApps)
142
+ })
143
+ }
144
+
145
+
150
146
  useLayoutEffect(() => {
151
147
  console.log(`Taruvi Navkit v${version} initialized`)
152
148
  const init = async () => {
@@ -189,7 +185,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
189
185
  }, [themeMode, onThemeChange])
190
186
 
191
187
  return (
192
- <NavigationContext.Provider value={{ navigateToUrl, isDesk, appsList, userData, siteSettings, appSettings, appSettingsLoaded, appName, chatUrl, sessionToken, isUserAuthenticated, client, auth, themeMode, toggleTheme, navbarColor, iconColor }}>
188
+ <NavigationContext.Provider value={{ navigateToUrl, isDesk, appsList, userData, siteSettings: siteSettings.current, appSettings, appSettingsLoaded, appName, jwtToken, isUserAuthenticated, client, auth, themeMode, toggleTheme, navbarColor, iconColor, fetchUserAppsList }}>
193
189
  {children}
194
190
  </NavigationContext.Provider>
195
191
  )
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from "react"
1
+ import { useState, useRef, useMemo } from "react"
2
2
  import type { AppData } from "../../types"
3
3
  import Search from "../Search/Search"
4
4
  import { Card, Box, Grid, Typography } from "@mui/material"
@@ -19,26 +19,49 @@ const isValidFaIcon = (icon: string): boolean => {
19
19
  }
20
20
 
21
21
  const AppLauncher = () => {
22
- const { navigateToUrl, appsList: allApps, themeMode } = useNavigation()
23
- const appsList = allApps.filter(a => a.id !== 'chat')
22
+ const { navigateToUrl, appsList, themeMode, fetchUserAppsList } = useNavigation()
24
23
  const styles = getAppLauncherStyles(themeMode)
25
- const [filteredApps, setFilteredApps] = useState<AppData[]>(appsList || [])
24
+ const [query, setQuery] = useState('')
25
+
26
+ // Derive during render — no second state, no effect, no loop
27
+ const filteredApps = useMemo(
28
+ () => appsList.filter(app =>
29
+ app.appname?.toLowerCase().includes(query.toLowerCase())
30
+ ),
31
+ [appsList, query]
32
+ )
33
+ // Single-flight guard for fetchUserAppsList.
34
+ // This prevents multiple concurrent calls to fetchUserAppsList.
35
+ const refreshPromise = useRef<Promise<void> | null>(null)
26
36
 
27
- const handleAppClick = (app: AppData) => {
28
- navigateToUrl(app.url, false)
37
+ const fetchIcons = async () => {
38
+ // A refresh is already running — share it instead of starting another.
39
+ if (refreshPromise.current) {
40
+ await refreshPromise.current
41
+ return
42
+ }
43
+
44
+ refreshPromise.current = (async () => {
45
+ try {
46
+ await fetchUserAppsList()
47
+ } finally {
48
+ // Release the guard so the next expiry cycle can refresh again.
49
+ refreshPromise.current = null
50
+ }
51
+ })()
52
+
53
+ await refreshPromise.current
29
54
  }
30
55
 
31
- const handleSearchChange = (filtered: AppData[]) => {
32
- setFilteredApps(filtered)
56
+ const handleAppClick = (app: AppData) => {
57
+ navigateToUrl(app.url, false)
33
58
  }
34
59
 
35
- useEffect(() => {
36
- setFilteredApps(appsList || [])
37
- }, [appsList])
60
+ const handleSearchChange = (q: string) => setQuery(q)
38
61
 
39
62
  return (
40
63
  <Card sx={styles.container}>
41
- <Search appsList={appsList} onSearchChange={handleSearchChange} />
64
+ <Search onSearchChange={handleSearchChange} />
42
65
  {filteredApps.filter(app => app.url).length === 0 ? (
43
66
  <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
44
67
  <Typography sx={{ color: styles.appName.color, fontSize: typography.sizes.sm }}>No apps available</Typography>
@@ -60,6 +83,7 @@ const AppLauncher = () => {
60
83
  <img
61
84
  src={app.icon}
62
85
  alt={app.appname}
86
+ onError={fetchIcons}
63
87
  style={{
64
88
  width: 40,
65
89
  height: 40,
@@ -94,4 +118,4 @@ const AppLauncher = () => {
94
118
  )
95
119
  }
96
120
 
97
- export default AppLauncher
121
+ export default AppLauncher
@@ -3,7 +3,6 @@ import { Box } from '@mui/material'
3
3
 
4
4
  interface Props {
5
5
  children: React.ReactNode
6
- titleBarActions?: React.ReactNode
7
6
  initialWidth?: number
8
7
  initialHeight?: number
9
8
  minWidth?: number
@@ -15,7 +14,6 @@ const HEADER_HEIGHT = 28
15
14
 
16
15
  const DraggableResizable = ({
17
16
  children,
18
- titleBarActions,
19
17
  initialWidth = 900,
20
18
  initialHeight = 600,
21
19
  minWidth = 320,
@@ -110,21 +108,13 @@ const DraggableResizable = ({
110
108
  cursor: 'grab',
111
109
  display: 'flex',
112
110
  alignItems: 'center',
113
- justifyContent: 'space-between',
114
- px: 1,
111
+ justifyContent: 'center',
115
112
  flexShrink: 0,
116
113
  userSelect: 'none',
117
114
  '&:active': { cursor: 'grabbing' },
118
115
  }}
119
116
  >
120
- <Box sx={{ width: 32 }} />
121
117
  <Box sx={{ width: 40, height: 4, borderRadius: 2, backgroundColor: '#999' }} />
122
- <Box
123
- sx={{ display: 'flex', alignItems: 'center', width: 32, justifyContent: 'flex-end' }}
124
- onPointerDown={(e) => e.stopPropagation()}
125
- >
126
- {titleBarActions}
127
- </Box>
128
118
  </Box>
129
119
  {/* Content */}
130
120
  <Box sx={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
@@ -1,46 +1,98 @@
1
- import {useLayoutEffect, useRef, useState} from 'react'
2
- import {Box, Badge} from '@mui/material'
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { Box, Badge, CircularProgress, Typography } from '@mui/material'
3
3
 
4
4
  interface MattermostChatProps {
5
- mattermostUrl: string // Mattermost server URL (must be passed from parent app)
6
- loginId: string // Mattermost login_id (username)
7
- sessionToken: string // Taruvi session token used as password for auth_type=session
5
+ jwtToken: string
6
+ mattermostUrl: string
7
+ loginId: string
8
8
  onNotification?: (count: number) => void
9
9
  onUrlClick?: (url: string) => void
10
10
  width?: string | number
11
11
  height?: string | number
12
12
  }
13
13
 
14
- const MattermostChat = ({ mattermostUrl, loginId, sessionToken, onNotification, onUrlClick, width = '100%', height = '100%' }: MattermostChatProps) => {
14
+ interface NotifyMessage {
15
+ event: string
16
+ data?: {
17
+ title?: string
18
+ body?: string
19
+ channel?: string
20
+ teamId?: string
21
+ url?: string
22
+ }
23
+ }
24
+
25
+ const MattermostChat = ({
26
+ jwtToken,
27
+ mattermostUrl,
28
+ loginId,
29
+ onNotification,
30
+ onUrlClick,
31
+ width = '100%',
32
+ height = '100%'
33
+ }: MattermostChatProps) => {
15
34
  const iframeRef = useRef<HTMLIFrameElement>(null)
16
- const credentialsRef = useRef({ loginId, sessionToken })
17
- credentialsRef.current = { loginId, sessionToken }
18
35
  const [unreadCount, setUnreadCount] = useState(0)
19
36
  const [isWindowFocused, setIsWindowFocused] = useState(true)
20
- const mattermostOrigin = new URL(mattermostUrl).origin
21
-
22
- const postLoginToBridge = () => {
23
- const { loginId: id, sessionToken: token } = credentialsRef.current
24
- if (!id || !token) return
25
- iframeRef.current?.contentWindow?.postMessage(
26
- { type: 'mm-login', loginId: id, token },
27
- mattermostOrigin
28
- )
29
- }
37
+ const [authToken, setAuthToken] = useState<string | null>(null)
38
+ const [authError, setAuthError] = useState<string | null>(null)
39
+ const [isLoading, setIsLoading] = useState(true)
30
40
 
31
- // useLayoutEffect so the listener is attached in the same commit as the iframe
32
- // before the bridge can fire mm-bridge-ready (useEffect runs too late and misses it).
33
- useLayoutEffect(() => {
34
- const handleMessage = (event: MessageEvent) => {
35
- if (event.origin !== mattermostOrigin) return
41
+ // Authenticate with Mattermost on mount
42
+ useEffect(() => {
43
+ const login = async () => {
44
+ try {
45
+ const res = await fetch(`${mattermostUrl.replace(/\/$/, '')}/api/v4/users/login`, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
48
+ credentials: 'omit',
49
+ body: JSON.stringify({ login_id: loginId, password: jwtToken, token: '', deviceId: '' })
50
+ })
51
+
52
+ if (!res.ok) {
53
+ setAuthError(`Login failed (${res.status})`)
54
+ setIsLoading(false)
55
+ return
56
+ }
36
57
 
37
- // Bridge ready — send login credentials
38
- if (event.data?.type === 'mm-bridge-ready') {
39
- postLoginToBridge()
40
- return
58
+ const token = res.headers.get('Token')
59
+ if (token) {
60
+ setAuthToken(token)
61
+ } else {
62
+ setAuthError('No token in response')
63
+ }
64
+ } catch (e: any) {
65
+ setAuthError(e.message || 'Login failed')
41
66
  }
67
+ setIsLoading(false)
68
+ }
69
+
70
+ if (loginId && jwtToken) login()
71
+ else { setAuthError('Missing credentials'); setIsLoading(false) }
72
+ }, [mattermostUrl, loginId, jwtToken])
73
+
74
+ // Inject auth token into iframe via postMessage once loaded
75
+ useEffect(() => {
76
+ if (!authToken || !iframeRef.current) return
77
+
78
+ const handleIframeLoad = () => {
79
+ // Set the auth cookie/token in the iframe context
80
+ iframeRef.current?.contentWindow?.postMessage(
81
+ { type: 'token', data: authToken },
82
+ new URL(mattermostUrl).origin
83
+ )
84
+ }
85
+
86
+ const iframe = iframeRef.current
87
+ iframe.addEventListener('load', handleIframeLoad)
88
+ return () => iframe.removeEventListener('load', handleIframeLoad)
89
+ }, [authToken, mattermostUrl])
90
+
91
+ useEffect(() => {
92
+ const handleMessage = (event: MessageEvent<NotifyMessage>) => {
93
+ const mattermostOrigin = new URL(mattermostUrl).origin
94
+ if (event.origin !== mattermostOrigin) return
42
95
 
43
- // Mattermost Notify events (unread badge, URL clicks)
44
96
  if (event.data?.event === 'Notify') {
45
97
  if (!isWindowFocused) {
46
98
  setUnreadCount(prev => {
@@ -49,74 +101,60 @@ const MattermostChat = ({ mattermostUrl, loginId, sessionToken, onNotification,
49
101
  return newCount
50
102
  })
51
103
  }
52
- if (event.data.data?.url && onUrlClick) {
53
- onUrlClick(event.data.data.url)
54
- }
104
+ if (event.data.data?.url && onUrlClick) onUrlClick(event.data.data.url)
55
105
  }
56
106
  }
57
107
 
58
- const handleFocus = () => {
59
- setIsWindowFocused(true)
60
- setUnreadCount(0)
61
- onNotification?.(0)
62
- }
63
-
108
+ const handleFocus = () => { setIsWindowFocused(true); setUnreadCount(0); onNotification?.(0) }
64
109
  const handleBlur = () => setIsWindowFocused(false)
65
110
 
66
111
  window.addEventListener('message', handleMessage)
67
112
  window.addEventListener('focus', handleFocus)
68
113
  window.addEventListener('blur', handleBlur)
69
-
70
114
  return () => {
71
115
  window.removeEventListener('message', handleMessage)
72
116
  window.removeEventListener('focus', handleFocus)
73
117
  window.removeEventListener('blur', handleBlur)
74
118
  }
75
- }, [mattermostOrigin, isWindowFocused, onNotification, onUrlClick])
119
+ }, [mattermostUrl, isWindowFocused, onNotification, onUrlClick])
120
+
121
+ if (isLoading) {
122
+ return (
123
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width, height }}>
124
+ <CircularProgress size={32} />
125
+ </Box>
126
+ )
127
+ }
128
+
129
+ if (authError) {
130
+ return (
131
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width, height }}>
132
+ <Typography color="error">{authError}</Typography>
133
+ </Box>
134
+ )
135
+ }
76
136
 
77
- // Always load the autologin bridge first it handles storage access + login,
78
- // then redirects the iframe to Mattermost
79
- const bridgeUrl = `${mattermostOrigin}/static/autologin.html`
137
+ // Load Mattermost with the auth token as a cookie header via the access_token param
138
+ const iframeUrl = `${mattermostUrl.replace(/\/$/, '')}?access_token=${authToken}`
80
139
 
81
140
  return (
82
- <Box
83
- sx={{
84
- position: 'relative',
85
- width,
86
- height,
87
- overflow: 'hidden'
88
- }}
89
- >
141
+ <Box sx={{ position: 'relative', width, height, overflow: 'hidden' }}>
90
142
  {unreadCount > 0 && (
91
143
  <Badge
92
144
  badgeContent={unreadCount}
93
145
  color="error"
94
146
  sx={{
95
- position: 'absolute',
96
- top: 16,
97
- right: 16,
98
- zIndex: 1000,
99
- '& .MuiBadge-badge': {
100
- fontSize: '1rem',
101
- height: '28px',
102
- minWidth: '28px',
103
- borderRadius: '14px'
104
- }
147
+ position: 'absolute', top: 16, right: 16, zIndex: 1000,
148
+ '& .MuiBadge-badge': { fontSize: '1rem', height: '28px', minWidth: '28px', borderRadius: '14px' }
105
149
  }}
106
150
  />
107
151
  )}
108
152
  <iframe
109
- key={`${loginId}:${sessionToken ? '1' : '0'}`}
110
153
  ref={iframeRef}
111
- src={bridgeUrl}
154
+ src={iframeUrl}
112
155
  title="Mattermost Chat"
113
- allow="clipboard-read; clipboard-write; storage-access"
114
- style={{
115
- width: '100%',
116
- height: '100%',
117
- border: 'none',
118
- display: 'block'
119
- }}
156
+ allow="clipboard-read; clipboard-write"
157
+ style={{ width: '100%', height: '100%', border: 'none', display: 'block' }}
120
158
  />
121
159
  </Box>
122
160
  )
@@ -6,7 +6,7 @@ import { getProfileStyles } from './Profile.styles'
6
6
  import { useNavigation } from '../../NavkitContext'
7
7
 
8
8
  const ProfileMenu = () => {
9
- const { auth, client, chatUrl, themeMode, toggleTheme, userData, siteSettings } = useNavigation()
9
+ const { auth, themeMode, toggleTheme, userData, siteSettings } = useNavigation()
10
10
  const styles = getProfileStyles(themeMode)
11
11
 
12
12
  // const handlePreferences = () => {
@@ -15,15 +15,6 @@ const ProfileMenu = () => {
15
15
 
16
16
  const handleLogout = async () => {
17
17
  localStorage.removeItem('navkit-theme-mode')
18
- try {
19
- await fetch(chatUrl + 'api/v4/users/logout', {
20
- method: 'POST',
21
- headers: {
22
- 'X-Session-Token': client.tokenClient.getSessionToken() || '',
23
- 'X-Requested-With': 'XMLHttpRequest',
24
- },
25
- })
26
- } catch { /* ignore */ }
27
18
  await auth.logout()
28
19
  }
29
20
 
@@ -3,14 +3,12 @@ import { TextField, InputAdornment } from '@mui/material'
3
3
  import { useState } from 'react'
4
4
  import { getSearchStyles } from './Search.styles'
5
5
  import { useNavigation } from '../../NavkitContext'
6
- import type { AppData } from '../../types'
7
6
 
8
7
  interface SearchProps {
9
- appsList?: AppData[]
10
- onSearchChange?: (filteredApps: AppData[]) => void
8
+ onSearchChange?: (query: string) => void
11
9
  }
12
10
 
13
- const Search = ({ appsList, onSearchChange }: SearchProps) => {
11
+ const Search = ({ onSearchChange }: SearchProps) => {
14
12
  const [searchTerm, setSearchTerm] = useState('')
15
13
  const { themeMode } = useNavigation()
16
14
  const styles = getSearchStyles(themeMode)
@@ -19,17 +17,8 @@ const Search = ({ appsList, onSearchChange }: SearchProps) => {
19
17
  const value = event.target.value
20
18
  setSearchTerm(value)
21
19
 
22
- // Filter apps based on search term
23
- if (appsList) {
24
- const filtered = value.trim() === ''
25
- ? appsList
26
- : appsList.filter(app =>
27
- app.appname.toLowerCase().includes(value.toLowerCase())
28
- )
29
-
30
- // Call parent callback with filtered results
31
- onSearchChange?.(filtered)
32
- }
20
+ // Report the raw query upward; filtering happens in the parent
21
+ onSearchChange?.(value)
33
22
  }
34
23
 
35
24
  return (