@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,227 @@
1
+ # MattermostChat Component
2
+
3
+ A React component that embeds Mattermost chat in an iframe with JWT authentication, notification tracking, and postMessage communication.
4
+
5
+ ## Features
6
+
7
+ - 🔐 JWT token authentication via URL parameter
8
+ - 🔔 Unread message notification badge
9
+ - 📋 Clipboard permissions (read/write)
10
+ - 🎯 postMessage listener for Mattermost events
11
+ - 🔒 Origin validation for security
12
+ - 📱 Responsive and customizable dimensions
13
+ - ♻️ Proper cleanup of event listeners
14
+
15
+ ## Installation
16
+
17
+ The component is already included in your project. Make sure you have the required dependencies:
18
+
19
+ ```bash
20
+ npm install @mui/material @emotion/react @emotion/styled
21
+ ```
22
+
23
+ ## Environment Variables
24
+
25
+ Create a `.env` file in your project root:
26
+
27
+ ```env
28
+ VITE_MATTERMOST_URL=http://localhost:8065
29
+ ```
30
+
31
+ For production, update this to your actual Mattermost server URL.
32
+
33
+ ## Usage
34
+
35
+ ### Basic Example
36
+
37
+ ```tsx
38
+ import { MattermostChat } from './components'
39
+
40
+ function App() {
41
+ const jwtToken = 'your-jwt-token-here'
42
+
43
+ return (
44
+ <div style={{ width: '100vw', height: '100vh' }}>
45
+ <MattermostChat jwtToken={jwtToken} />
46
+ </div>
47
+ )
48
+ }
49
+ ```
50
+
51
+ ### Advanced Example with Callbacks
52
+
53
+ ```tsx
54
+ import { MattermostChat } from './components'
55
+ import { useState } from 'react'
56
+
57
+ function ChatPage() {
58
+ const [notificationCount, setNotificationCount] = useState(0)
59
+ const jwtToken = 'your-jwt-token-here'
60
+
61
+ const handleNotification = (count: number) => {
62
+ console.log(`Unread messages: ${count}`)
63
+ setNotificationCount(count)
64
+
65
+ // You can trigger browser notifications here
66
+ if (count > 0 && Notification.permission === 'granted') {
67
+ new Notification('New Mattermost Message', {
68
+ body: `You have ${count} unread message(s)`
69
+ })
70
+ }
71
+ }
72
+
73
+ const handleUrlClick = (url: string) => {
74
+ console.log(`User clicked URL in Mattermost: ${url}`)
75
+ // Handle navigation or custom logic
76
+ }
77
+
78
+ return (
79
+ <div style={{ width: '100%', height: 'calc(100vh - 64px)' }}>
80
+ <MattermostChat
81
+ jwtToken={jwtToken}
82
+ onNotification={handleNotification}
83
+ onUrlClick={handleUrlClick}
84
+ width="100%"
85
+ height="100%"
86
+ />
87
+ </div>
88
+ )
89
+ }
90
+ ```
91
+
92
+ ### Custom Dimensions
93
+
94
+ ```tsx
95
+ <MattermostChat
96
+ jwtToken={jwtToken}
97
+ width="800px"
98
+ height="600px"
99
+ />
100
+ ```
101
+
102
+ ### Integration with NavKit
103
+
104
+ ```tsx
105
+ // In your App.tsx or main component
106
+ import { useState } from 'react'
107
+ import { MattermostChat } from './components'
108
+
109
+ function NavkitApp() {
110
+ const [showChat, setShowChat] = useState(false)
111
+ const jwtToken = 'your-jwt-token'
112
+
113
+ return (
114
+ <>
115
+ {/* Your existing NavKit code */}
116
+ <AppBar>
117
+ <Toolbar>
118
+ <IconButton onClick={() => setShowChat(!showChat)}>
119
+ <ChatIcon />
120
+ </IconButton>
121
+ </Toolbar>
122
+ </AppBar>
123
+
124
+ {/* Mattermost Chat Modal or Sidebar */}
125
+ {showChat && (
126
+ <Box
127
+ sx={{
128
+ position: 'fixed',
129
+ right: 0,
130
+ top: 64,
131
+ width: 400,
132
+ height: 'calc(100vh - 64px)',
133
+ zIndex: 1200,
134
+ boxShadow: 3
135
+ }}
136
+ >
137
+ <MattermostChat jwtToken={jwtToken} />
138
+ </Box>
139
+ )}
140
+ </>
141
+ )
142
+ }
143
+ ```
144
+
145
+ ## Props
146
+
147
+ | Prop | Type | Required | Default | Description |
148
+ |------|------|----------|---------|-------------|
149
+ | `jwtToken` | `string` | Yes | - | JWT token for Mattermost authentication |
150
+ | `onNotification` | `(count: number) => void` | No | - | Callback when notification count changes |
151
+ | `onUrlClick` | `(url: string) => void` | No | - | Callback when user clicks a URL in Mattermost |
152
+ | `width` | `string \| number` | No | `'100%'` | Width of the iframe container |
153
+ | `height` | `string \| number` | No | `'100%'` | Height of the iframe container |
154
+
155
+ ## How It Works
156
+
157
+ ### Authentication
158
+ The component constructs the iframe URL as:
159
+ ```
160
+ ${VITE_MATTERMOST_URL}/login?oxauth=${jwtToken}
161
+ ```
162
+
163
+ This allows automatic authentication with your Mattermost server using the provided JWT token.
164
+
165
+ ### Notification Tracking
166
+ - When the window loses focus (user switches tabs/windows), incoming Mattermost messages increment the unread counter
167
+ - A red badge appears in the top-right corner showing the unread count
168
+ - When the window regains focus, the counter resets to 0
169
+ - The `onNotification` callback is triggered whenever the count changes
170
+
171
+ ### Security
172
+ - Origin validation ensures only messages from your configured Mattermost server are processed
173
+ - Messages from other origins are logged and ignored
174
+ - Clipboard permissions are explicitly granted for proper copy/paste functionality
175
+
176
+ ### postMessage Communication
177
+ The component listens for `Notify` events from the Mattermost iframe with the following structure:
178
+
179
+ ```typescript
180
+ {
181
+ event: 'Notify',
182
+ data: {
183
+ title: 'Message title',
184
+ body: 'Message content',
185
+ channel: 'channel-id',
186
+ teamId: 'team-id',
187
+ url: 'deep-link-url'
188
+ }
189
+ }
190
+ ```
191
+
192
+ ## Browser Compatibility
193
+
194
+ - Chrome/Edge: ✅ Full support
195
+ - Firefox: ✅ Full support
196
+ - Safari: ✅ Full support
197
+ - Mobile browsers: ✅ Full support (with responsive sizing)
198
+
199
+ ## Security Considerations
200
+
201
+ 1. **JWT Token Storage**: Never store JWT tokens in localStorage or sessionStorage if they contain sensitive data. Consider using httpOnly cookies or secure session management.
202
+
203
+ 2. **Origin Validation**: The component validates the origin of postMessage events. Ensure `VITE_MATTERMOST_URL` is set correctly in production.
204
+
205
+ 3. **HTTPS**: In production, always use HTTPS for both your app and Mattermost server.
206
+
207
+ ## Troubleshooting
208
+
209
+ ### Iframe not loading
210
+ - Check that `VITE_MATTERMOST_URL` is set correctly in `.env`
211
+ - Verify the JWT token is valid
212
+ - Check browser console for CORS or CSP errors
213
+ - Ensure Mattermost server allows iframe embedding
214
+
215
+ ### Notifications not working
216
+ - Verify the window focus/blur events are firing
217
+ - Check that postMessage events are being sent from Mattermost
218
+ - Ensure origin validation is passing (check console for warnings)
219
+
220
+ ### Clipboard not working
221
+ - Verify the `allow` attribute includes clipboard permissions
222
+ - Check that the user has granted clipboard permissions in their browser
223
+ - Some browsers require HTTPS for clipboard API access
224
+
225
+ ## License
226
+
227
+ Part of the Taruvi NavKit project.
@@ -0,0 +1,59 @@
1
+ import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
2
+
3
+ export const profileStyles = {
4
+ container: {
5
+ position: 'relative' as const,
6
+ },
7
+ trigger: {
8
+ display: 'flex',
9
+ alignItems: 'center',
10
+ gap: spacing.sm,
11
+ ml: spacing.md,
12
+ cursor: 'pointer',
13
+ },
14
+ avatar: {
15
+ width: dimensions.avatarSize,
16
+ height: dimensions.avatarSize,
17
+ bgcolor: colours.bg.avatar,
18
+ color: colours.text.primary,
19
+ fontWeight: typography.weights.semibold,
20
+ fontSize: typography.sizes.sm,
21
+ },
22
+ name: {
23
+ color: colours.text.primary,
24
+ fontSize: typography.sizes.sm,
25
+ fontWeight: typography.weights.regular,
26
+ letterSpacing: typography.letterSpacing.default,
27
+ display: { xs: 'none', md: 'block' },
28
+ },
29
+ menu: {
30
+ position: 'absolute' as const,
31
+ top: 60,
32
+ right: 0,
33
+ width: 200,
34
+ borderRadius: borderRadius.default,
35
+ boxShadow: shadows.dropdown,
36
+ backgroundColor: colours.bg.white,
37
+ zIndex: zIndex.overlay,
38
+ py: 1,
39
+ },
40
+ menuItem: {
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ gap: spacing.md,
44
+ px: spacing.md,
45
+ py: spacing.sm,
46
+ '&:hover': {
47
+ backgroundColor: colours.bg.light,
48
+ },
49
+ },
50
+ menuItemText: {
51
+ color: colours.text.secondary,
52
+ fontSize: typography.sizes.sm,
53
+ fontWeight: typography.weights.regular,
54
+ },
55
+ iconStyle: {
56
+ fontSize: dimensions.iconSize.sm,
57
+ color: colours.text.secondary,
58
+ },
59
+ }
@@ -0,0 +1,36 @@
1
+ import { Box, Avatar, Typography } from '@mui/material'
2
+ import { profileStyles } from './Profile.styles'
3
+ import nopfp from "../../assets/nopfp.png"
4
+
5
+ interface UserData {
6
+ full_name?: string
7
+ first_name?: string
8
+ last_name?: string
9
+ pfp?: string
10
+ username?: string
11
+ email?: string
12
+ }
13
+
14
+ interface ProfileProps {
15
+ userData: UserData
16
+ onClick: () => void
17
+ }
18
+
19
+ const Profile = (props: ProfileProps) => {
20
+ const { userData, onClick } = props
21
+
22
+ return (
23
+ <Box sx={profileStyles.trigger} onClick={onClick}>
24
+ <Avatar
25
+ src={userData.pfp || nopfp}
26
+ sx={profileStyles.avatar}
27
+ >
28
+ </Avatar>
29
+ <Typography sx={profileStyles.name}>
30
+ {userData.first_name} {userData.last_name}
31
+ </Typography>
32
+ </Box>
33
+ )
34
+ }
35
+
36
+ export default Profile
@@ -0,0 +1,42 @@
1
+ import { Card, MenuItem, Typography } from '@mui/material'
2
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3
+ import { profileStyles } from './Profile.styles'
4
+ import { useNavigation } from '../../NavkitContext'
5
+
6
+ const ProfileMenu = () => {
7
+ const { navigateToUrl } = useNavigation()
8
+
9
+ const handlePreferences = () => {
10
+ navigateToUrl('preferences', true)
11
+ }
12
+
13
+ const handleLogout = async () => {
14
+ //handle logout
15
+ }
16
+
17
+ return (
18
+ <Card sx={profileStyles.menu}>
19
+ <MenuItem onClick={handlePreferences} sx={profileStyles.menuItem}>
20
+ <FontAwesomeIcon
21
+ icon={["fas", "gear"]}
22
+ style={profileStyles.iconStyle}
23
+ />
24
+ <Typography sx={profileStyles.menuItemText}>
25
+ Preferences
26
+ </Typography>
27
+ </MenuItem>
28
+
29
+ <MenuItem onClick={handleLogout} sx={profileStyles.menuItem}>
30
+ <FontAwesomeIcon
31
+ icon={["fas", "right-from-bracket"]}
32
+ style={profileStyles.iconStyle}
33
+ />
34
+ <Typography sx={profileStyles.menuItemText}>
35
+ Logout
36
+ </Typography>
37
+ </MenuItem>
38
+ </Card>
39
+ )
40
+ }
41
+
42
+ export default ProfileMenu
@@ -0,0 +1,7 @@
1
+ export const searchStyles = {
2
+ textField: {
3
+ '& .MuiOutlinedInput-root': {
4
+ height: '40px',
5
+ },
6
+ },
7
+ }
@@ -0,0 +1,51 @@
1
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2
+ import { TextField, InputAdornment } from '@mui/material'
3
+ import { useState } from 'react'
4
+ import { searchStyles } from './Search.styles'
5
+ import type { AppData } from '../../types'
6
+
7
+ interface SearchProps {
8
+ appsList?: AppData[]
9
+ onSearchChange?: (filteredApps: AppData[]) => void
10
+ }
11
+
12
+ const Search = ({ appsList, onSearchChange }: SearchProps) => {
13
+ const [searchTerm, setSearchTerm] = useState('')
14
+
15
+ const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
16
+ const value = event.target.value
17
+ setSearchTerm(value)
18
+
19
+ // Filter apps based on search term
20
+ if (appsList) {
21
+ const filtered = value.trim() === ''
22
+ ? appsList
23
+ : appsList.filter(app =>
24
+ app.appname.toLowerCase().includes(value.toLowerCase())
25
+ )
26
+
27
+ // Call parent callback with filtered results
28
+ onSearchChange?.(filtered)
29
+ }
30
+ }
31
+
32
+ return (
33
+ <TextField
34
+ fullWidth
35
+ placeholder="Search for apps"
36
+ variant="outlined"
37
+ value={searchTerm}
38
+ onChange={handleSearchChange}
39
+ sx={searchStyles.textField}
40
+ InputProps={{
41
+ startAdornment: (
42
+ <InputAdornment position="start">
43
+ <FontAwesomeIcon icon={["fas", "magnifying-glass"]} style={{ color: '#9e9e9e' }} />
44
+ </InputAdornment>
45
+ ),
46
+ }}
47
+ />
48
+ )
49
+ }
50
+
51
+ export default Search
@@ -0,0 +1,29 @@
1
+ import { colours, spacing, dimensions } from '../../styles/variables'
2
+
3
+ export const shortcutsStyles = {
4
+ desktopContainer: {
5
+ display: { xs: 'none', md: 'flex' },
6
+ alignItems: 'center',
7
+ gap: 3,
8
+ ml: spacing.md,
9
+ },
10
+ mobileTrigger: {
11
+ display: { xs: 'flex', md: 'none' },
12
+ ml: spacing.sm,
13
+ },
14
+ shortcut: {
15
+ display: 'flex',
16
+ flexDirection: 'column' as const,
17
+ alignItems: 'center',
18
+ textDecoration: 'none',
19
+ cursor: 'pointer',
20
+ color: colours.text.secondary,
21
+ '&:hover': {
22
+ opacity: 0.7,
23
+ },
24
+ },
25
+ iconStyle: {
26
+ fontSize: dimensions.iconSize.lg,
27
+ color: colours.text.secondary,
28
+ },
29
+ }
@@ -0,0 +1,72 @@
1
+ import type { AppData } from '../../types'
2
+ import { Box, Link, IconButton } from '@mui/material'
3
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4
+ import { shortcutsStyles } from './Shortcuts.styles'
5
+ import { useNavigation } from '../../NavkitContext'
6
+
7
+ interface ShortcutsProps {
8
+ showChat?: (showChat: boolean) => void
9
+ onMenuToggle?: () => void
10
+ triggerRef?: React.RefObject<HTMLButtonElement | null>
11
+ }
12
+
13
+ const Shortcuts = (props: ShortcutsProps) => {
14
+
15
+ const { showChat, onMenuToggle, triggerRef } = props
16
+ const { navigateToUrl, siteSettings, isUserAuthenticated } = useNavigation()
17
+ const shortcuts = siteSettings.shortcuts
18
+
19
+ const handleShortcutClick = (shortcut: AppData) => {
20
+ if (shortcut.url) {
21
+ navigateToUrl(shortcut.url, false)
22
+ }
23
+ }
24
+ if ((!shortcuts || shortcuts.length === 0) && !siteSettings.showChat) {
25
+ return null
26
+ }
27
+
28
+ return (
29
+ <>
30
+ {/* Desktop view - inline shortcuts */}
31
+ <Box sx={shortcutsStyles.desktopContainer}>
32
+ {shortcuts?.map((shortcut) => (
33
+ <Link
34
+ key={shortcut.id}
35
+ onClick={() => handleShortcutClick(shortcut)}
36
+ sx={shortcutsStyles.shortcut}
37
+ >
38
+ <FontAwesomeIcon
39
+ icon={["far", shortcut.icon] as any}
40
+ style={shortcutsStyles.iconStyle}
41
+ />
42
+ </Link>
43
+ ))}
44
+ {siteSettings.showChat && isUserAuthenticated && <Link
45
+ key={"chat"}
46
+ onClick={() => showChat?.(true)}
47
+ sx={shortcutsStyles.shortcut}
48
+ >
49
+ <FontAwesomeIcon
50
+ icon={["far", "comment"] as any}
51
+ style={shortcutsStyles.iconStyle}
52
+ />
53
+ </Link>
54
+ }
55
+ </Box>
56
+
57
+ {/* Mobile view - trigger button */}
58
+ <IconButton
59
+ ref={triggerRef}
60
+ onClick={onMenuToggle}
61
+ sx={shortcutsStyles.mobileTrigger}
62
+ >
63
+ <FontAwesomeIcon
64
+ icon={["fas", "ellipsis-v"]}
65
+ style={shortcutsStyles.iconStyle}
66
+ />
67
+ </IconButton>
68
+ </>
69
+ )
70
+ }
71
+
72
+ export default Shortcuts
@@ -0,0 +1,34 @@
1
+ import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
2
+
3
+ export const shortcutsMenuStyles = {
4
+ menu: {
5
+ position: 'absolute' as const,
6
+ top: 60,
7
+ right: 0,
8
+ width: 200,
9
+ borderRadius: borderRadius.default,
10
+ boxShadow: shadows.dropdown,
11
+ backgroundColor: colours.bg.white,
12
+ zIndex: zIndex.overlay,
13
+ py: 1,
14
+ },
15
+ menuItem: {
16
+ display: 'flex',
17
+ alignItems: 'center',
18
+ gap: spacing.md,
19
+ px: spacing.md,
20
+ py: spacing.sm,
21
+ '&:hover': {
22
+ backgroundColor: colours.bg.light,
23
+ },
24
+ },
25
+ menuItemText: {
26
+ color: colours.text.secondary,
27
+ fontSize: typography.sizes.sm,
28
+ fontWeight: typography.weights.regular,
29
+ },
30
+ iconStyle: {
31
+ fontSize: dimensions.iconSize.sm,
32
+ color: colours.text.secondary,
33
+ },
34
+ }
@@ -0,0 +1,62 @@
1
+ import { Card, MenuItem, Typography } from '@mui/material'
2
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3
+ import type { AppData } from '../../types'
4
+ import { shortcutsMenuStyles } from './ShortcutsMenu.styles'
5
+ import { useNavigation } from '../../NavkitContext'
6
+
7
+ interface ShortcutsMenuProps {
8
+ showChat: (showChat: boolean) => void
9
+ }
10
+
11
+ const ShortcutsMenu = (props: ShortcutsMenuProps) => {
12
+ const { showChat } = props
13
+ const { navigateToUrl, siteSettings, isUserAuthenticated } = useNavigation()
14
+ const shortcuts = siteSettings.shortcuts
15
+
16
+ const handleShortcutClick = (shortcut: AppData) => {
17
+ if (shortcut.url) {
18
+ navigateToUrl(shortcut.url, false)
19
+ }
20
+ }
21
+
22
+ if ((!shortcuts || shortcuts.length === 0) && !siteSettings.showChat) {
23
+ return null
24
+ }
25
+
26
+ return (
27
+ <Card sx={shortcutsMenuStyles.menu}>
28
+ {siteSettings.showChat && isUserAuthenticated && (
29
+ <MenuItem
30
+ key="chat"
31
+ onClick={() => showChat(true)}
32
+ sx={shortcutsMenuStyles.menuItem}
33
+ >
34
+ <FontAwesomeIcon
35
+ icon={["far", "comment"] as any}
36
+ style={shortcutsMenuStyles.iconStyle}
37
+ />
38
+ <Typography sx={shortcutsMenuStyles.menuItemText}>
39
+ Chat
40
+ </Typography>
41
+ </MenuItem>
42
+ )}
43
+ {shortcuts?.map((shortcut) => (
44
+ <MenuItem
45
+ key={shortcut.id}
46
+ onClick={() => handleShortcutClick(shortcut)}
47
+ sx={shortcutsMenuStyles.menuItem}
48
+ >
49
+ <FontAwesomeIcon
50
+ icon={["far", shortcut.icon] as any}
51
+ style={shortcutsMenuStyles.iconStyle}
52
+ />
53
+ <Typography sx={shortcutsMenuStyles.menuItemText}>
54
+ {shortcut.appname}
55
+ </Typography>
56
+ </MenuItem>
57
+ ))}
58
+ </Card>
59
+ )
60
+ }
61
+
62
+ export default ShortcutsMenu
@@ -0,0 +1,8 @@
1
+ import AppLauncher from './AppLauncher/AppLauncher'
2
+ import Shortcuts from './Shortucts/Shortcuts'
3
+ import ShortcutsMenu from './Shortucts/ShortcutsMenu'
4
+ import Profile from './Profile/Profile'
5
+ import ProfileMenu from './Profile/ProfileMenu'
6
+ import MattermostChat from './MattermostChat/MattermostChat'
7
+
8
+ export { AppLauncher, Shortcuts, ShortcutsMenu, Profile, ProfileMenu, MattermostChat }
@@ -0,0 +1,42 @@
1
+ // Common Style Patterns - Reusable style objects
2
+
3
+ export const flexCenter = {
4
+ display: 'flex',
5
+ alignItems: 'center',
6
+ justifyContent: 'center',
7
+ }
8
+
9
+ export const flexRow = {
10
+ display: 'flex',
11
+ alignItems: 'center',
12
+ }
13
+
14
+ export const flexColumn = {
15
+ display: 'flex',
16
+ flexDirection: 'column' as const,
17
+ alignItems: 'center',
18
+ }
19
+
20
+ export const hoverFade = {
21
+ '&:hover': {
22
+ opacity: 0.7,
23
+ },
24
+ }
25
+
26
+ export const hoverBgLight = {
27
+ '&:hover': {
28
+ backgroundColor: '#f5f5f5',
29
+ },
30
+ }
31
+
32
+ export const cursorPointer = {
33
+ cursor: 'pointer',
34
+ }
35
+
36
+ export const absolutePositioned = {
37
+ position: 'absolute' as const,
38
+ }
39
+
40
+ export const relativePositioned = {
41
+ position: 'relative' as const,
42
+ }