@taruvi/navkit 0.0.48-beta.3 → 0.0.48-beta.5

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.3",
3
+ "version": "0.0.48-beta.5",
4
4
  "main": "src/App.tsx",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/App.tsx CHANGED
@@ -13,6 +13,23 @@ import taruviLogo from "./assets/logo.svg";
13
13
  import taruviLogoWhite from "./assets/taruvi-logo-white.png";
14
14
  import type { Client } from "@taruvi/sdk"
15
15
 
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
+
16
33
  const ChatIcon = ({ color }: { color?: string }) => (
17
34
  <SvgIcon viewBox="-2 -2 24 24" sx={{ fontSize: 23, display: 'block', position: 'relative', top: 2 }}>
18
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"} />
@@ -20,7 +37,7 @@ const ChatIcon = ({ color }: { color?: string }) => (
20
37
  )
21
38
 
22
39
  const NavkitContent = () => {
23
- const { isUserAuthenticated, appSettings, appSettingsLoaded, appName, userData, appsList, chatUrl, themeMode, navbarColor, iconColor } = useNavigation();
40
+ const { isUserAuthenticated, appSettings, appSettingsLoaded, appName, userData, appsList, chatUrl, themeMode, navbarColor, iconColor, sessionToken } = useNavigation();
24
41
  const resolvedAppName = appName || appSettings?.displayName
25
42
  const showTaruviLogo = appSettingsLoaded && !resolvedAppName && !appSettings?.icon
26
43
  const styles = getAppStyles(themeMode)
@@ -61,7 +78,13 @@ const NavkitContent = () => {
61
78
  <Box component={"div"} sx={styles.rightSection}>
62
79
  <Shortcuts onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)} iconColor={iconColor} />
63
80
  {isUserAuthenticated && appsList.some(a => a.id === 'chat') && chatUrl && (
64
- <Box onClick={() => setShowChat(true)} sx={{ ...styles.appLauncherContainer, cursor: 'pointer' }}>
81
+ <Box
82
+ onClick={() => {
83
+ requestMattermostStorageAccessFromUserGesture(chatUrl)
84
+ setShowChat(true)
85
+ }}
86
+ sx={{ ...styles.appLauncherContainer, cursor: 'pointer' }}
87
+ >
65
88
  <ChatIcon color={iconColor} />
66
89
  </Box>
67
90
  )}
@@ -122,7 +145,13 @@ const NavkitContent = () => {
122
145
  <FontAwesomeIcon icon={["fas", "external-link-alt"]} style={{ fontSize: "10px" }} />
123
146
  </IconButton>
124
147
  </Box>
125
- <MattermostChat mattermostUrl={chatUrl} width="100%" height="100%" />
148
+ <MattermostChat
149
+ mattermostUrl={chatUrl}
150
+ loginId={userData?.username ?? ''}
151
+ sessionToken={sessionToken ?? ''}
152
+ width="100%"
153
+ height="100%"
154
+ />
126
155
  </DraggableResizable>
127
156
  )}
128
157
  </>
@@ -129,17 +129,6 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
129
129
  const userDataResponse = await auth.getCurrentUser()
130
130
  setUserData(userDataResponse?.data || null)
131
131
 
132
- // Login to Mattermost on init if chat is configured
133
- const token = auth.getSessionToken() || ''
134
- if (chatUrl && token && userDataResponse?.data?.username) {
135
- fetch(`${chatUrl.replace(/\/$/, '')}/api/v4/users/login`, {
136
- method: 'POST',
137
- headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-Environment': 'Browser' },
138
- credentials: 'include',
139
- body: JSON.stringify({ login_id: userDataResponse.data.username, password: token, auth_type: "session" })
140
- }).catch(() => {})
141
- }
142
-
143
132
  // Fetch user apps using username from userData
144
133
  if (userDataResponse?.data?.username) {
145
134
  const appsResponse = await user.current.getUserApps?.(userDataResponse.data.username)
@@ -1,41 +1,46 @@
1
- import {useEffect, useRef, useState} from 'react'
1
+ import {useLayoutEffect, useRef, useState} from 'react'
2
2
  import {Box, Badge} from '@mui/material'
3
3
 
4
4
  interface MattermostChatProps {
5
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
6
8
  onNotification?: (count: number) => void
7
9
  onUrlClick?: (url: string) => void
8
10
  width?: string | number
9
11
  height?: string | number
10
12
  }
11
13
 
12
- interface NotifyMessage {
13
- event: string
14
- data?: {
15
- title?: string
16
- body?: string
17
- channel?: string
18
- teamId?: string
19
- url?: string
20
- }
21
- }
22
-
23
- const MattermostChat = ({ mattermostUrl, onNotification, onUrlClick, width = '100%', height = '100%' }: MattermostChatProps) => {
14
+ const MattermostChat = ({ mattermostUrl, loginId, sessionToken, onNotification, onUrlClick, width = '100%', height = '100%' }: MattermostChatProps) => {
24
15
  const iframeRef = useRef<HTMLIFrameElement>(null)
16
+ const credentialsRef = useRef({ loginId, sessionToken })
17
+ credentialsRef.current = { loginId, sessionToken }
25
18
  const [unreadCount, setUnreadCount] = useState(0)
26
19
  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
+ }
30
+
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
27
36
 
28
- useEffect(() => {
29
- // Handle postMessage events from Mattermost iframe
30
- const handleMessage = (event: MessageEvent<NotifyMessage>) => {
31
- // Validate origin for security
32
- const mattermostOrigin = new URL(mattermostUrl).origin
33
- if (event.origin !== mattermostOrigin) {
34
- console.warn('Received message from untrusted origin:', event.origin)
37
+ // Bridge ready — send login credentials
38
+ if (event.data?.type === 'mm-bridge-ready') {
39
+ postLoginToBridge()
35
40
  return
36
41
  }
37
42
 
38
- // Handle Notify events
43
+ // Mattermost Notify events (unread badge, URL clicks)
39
44
  if (event.data?.event === 'Notify') {
40
45
  if (!isWindowFocused) {
41
46
  setUnreadCount(prev => {
@@ -44,41 +49,34 @@ const MattermostChat = ({ mattermostUrl, onNotification, onUrlClick, width = '10
44
49
  return newCount
45
50
  })
46
51
  }
47
-
48
- // Handle URL clicks if provided
49
52
  if (event.data.data?.url && onUrlClick) {
50
53
  onUrlClick(event.data.data.url)
51
54
  }
52
55
  }
53
56
  }
54
57
 
55
- // Handle window focus/blur to track unread messages
56
58
  const handleFocus = () => {
57
59
  setIsWindowFocused(true)
58
60
  setUnreadCount(0)
59
61
  onNotification?.(0)
60
62
  }
61
63
 
62
- const handleBlur = () => {
63
- setIsWindowFocused(false)
64
- }
64
+ const handleBlur = () => setIsWindowFocused(false)
65
65
 
66
- // Add event listeners
67
66
  window.addEventListener('message', handleMessage)
68
67
  window.addEventListener('focus', handleFocus)
69
68
  window.addEventListener('blur', handleBlur)
70
69
 
71
- // Cleanup event listeners on unmount
72
70
  return () => {
73
71
  window.removeEventListener('message', handleMessage)
74
72
  window.removeEventListener('focus', handleFocus)
75
73
  window.removeEventListener('blur', handleBlur)
76
74
  }
77
- }, [mattermostUrl, isWindowFocused, onNotification, onUrlClick])
75
+ }, [mattermostOrigin, isWindowFocused, onNotification, onUrlClick])
78
76
 
79
- // Construct iframe URL with JWT authentication
80
- // const iframeUrl = `${mattermostUrl.replace(/\/$/, '')}/login`
81
- const iframeUrl = mattermostUrl
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`
82
80
 
83
81
  return (
84
82
  <Box
@@ -108,10 +106,11 @@ const MattermostChat = ({ mattermostUrl, onNotification, onUrlClick, width = '10
108
106
  />
109
107
  )}
110
108
  <iframe
109
+ key={`${loginId}:${sessionToken ? '1' : '0'}`}
111
110
  ref={iframeRef}
112
- src={iframeUrl}
111
+ src={bridgeUrl}
113
112
  title="Mattermost Chat"
114
- allow="clipboard-read; clipboard-write"
113
+ allow="clipboard-read; clipboard-write; storage-access"
115
114
  style={{
116
115
  width: '100%',
117
116
  height: '100%',