1import './tab-bar.css' 2import * as ConfigGen from '../actions/config-gen' 3import * as LoginGen from '../actions/login-gen' 4import * as Container from '../util/container' 5import * as FsConstants from '../constants/fs' 6import * as Kb from '../common-adapters' 7import * as Kbfs from '../fs/common' 8import * as Platforms from '../constants/platform' 9import * as ProfileGen from '../actions/profile-gen' 10import * as ProvisionGen from '../actions/provision-gen' 11import * as RPCTypes from '../constants/types/rpc-gen' 12import * as React from 'react' 13import * as RouteTreeGen from '../actions/route-tree-gen' 14import * as SettingsConstants from '../constants/settings' 15import * as SettingsGen from '../actions/settings-gen' 16import * as Styles from '../styles' 17import * as Tabs from '../constants/tabs' 18import * as TrackerConstants from '../constants/tracker2' 19import flags from '../util/feature-flags' 20import AccountSwitcher from './account-switcher/container' 21import RuntimeStats from '../app/runtime-stats' 22import InviteFriends from '../people/invite-friends/tab-bar-button' 23import HiddenString from '../util/hidden-string' 24import openURL from '../util/open-url' 25import {isLinux} from '../constants/platform' 26import {quit} from '../desktop/app/ctl.desktop' 27import {tabRoots} from './routes' 28 29export type Props = { 30 navigation: any 31 selectedTab: Tabs.AppTab 32} 33 34const data = { 35 [Tabs.chatTab]: {icon: 'iconfont-nav-2-chat', label: 'Chat'}, 36 [Tabs.cryptoTab]: {icon: 'iconfont-nav-2-crypto', label: 'Crypto'}, 37 [Tabs.devicesTab]: {icon: 'iconfont-nav-2-devices', label: 'Devices'}, 38 [Tabs.fsTab]: {icon: 'iconfont-nav-2-files', label: 'Files'}, 39 [Tabs.gitTab]: {icon: 'iconfont-nav-2-git', label: 'Git'}, 40 [Tabs.peopleTab]: {icon: 'iconfont-nav-2-people', label: 'People'}, 41 [Tabs.settingsTab]: {icon: 'iconfont-nav-2-settings', label: 'Settings'}, 42 [Tabs.teamsTab]: {icon: 'iconfont-nav-2-teams', label: 'Teams'}, 43 [Tabs.walletsTab]: {icon: 'iconfont-nav-2-wallets', label: 'Wallet'}, 44} as const 45 46const tabs = Tabs.desktopTabOrder 47 48const FilesTabBadge = () => { 49 const uploadIcon = FsConstants.getUploadIconForFilesTab(Container.useSelector(state => state.fs.badge)) 50 return uploadIcon ? <Kbfs.UploadIcon uploadIcon={uploadIcon} style={styles.badgeIconUpload} /> : null 51} 52 53const Header = () => { 54 const dispatch = Container.useDispatch() 55 const [showingMenu, setShowingMenu] = React.useState(false) 56 const attachmentRef = React.useRef<Kb.Box2>(null) 57 const getAttachmentRef = () => attachmentRef.current 58 const fullname = Container.useSelector( 59 state => TrackerConstants.getDetails(state, state.config.username).fullname || '' 60 ) 61 const username = Container.useSelector(state => state.config.username) 62 const onProfileClick = () => dispatch(ProfileGen.createShowUserProfile({username})) 63 const onClickWrapper = () => { 64 setShowingMenu(false) 65 onProfileClick() 66 } 67 68 const onAddAccount = () => dispatch(ProvisionGen.createStartProvision()) 69 const onHelp = () => openURL('https://book.keybase.io') 70 const onQuit = () => { 71 if (!__DEV__) { 72 if (isLinux) { 73 dispatch(SettingsGen.createStop({exitCode: RPCTypes.ExitCode.ok})) 74 } else { 75 dispatch(ConfigGen.createDumpLogs({reason: 'quitting through menu'})) 76 } 77 } 78 // In case dump log doesn't exit for us 79 Electron.remote.getCurrentWindow().hide() 80 setTimeout(() => { 81 quit() 82 }, 2000) 83 } 84 const onSettings = () => dispatch(RouteTreeGen.createSwitchTab({tab: Tabs.settingsTab})) 85 const onSignOut = () => dispatch(RouteTreeGen.createNavigateAppend({path: [SettingsConstants.logOutTab]})) 86 87 const menuHeader = () => ( 88 <Kb.Box2 direction="vertical" fullWidth={true}> 89 <Kb.ClickableBox onClick={onClickWrapper} style={styles.headerBox}> 90 <Kb.ConnectedNameWithIcon 91 username={username} 92 onClick={onClickWrapper} 93 metaTwo={ 94 <Kb.Text type="BodySmall" lineClamp={1} style={styles.fullname}> 95 {fullname} 96 </Kb.Text> 97 } 98 /> 99 </Kb.ClickableBox> 100 <Kb.Button 101 label="View/Edit profile" 102 mode="Secondary" 103 onClick={onClickWrapper} 104 small={true} 105 style={styles.button} 106 /> 107 <AccountSwitcher /> 108 </Kb.Box2> 109 ) 110 111 const menuItems = (): Kb.MenuItems => [ 112 {onClick: onAddAccount, title: 'Log in as another user'}, 113 {onClick: onSettings, title: 'Settings'}, 114 {onClick: onHelp, title: 'Help'}, 115 {danger: true, onClick: onSignOut, title: 'Sign out'}, 116 {danger: true, onClick: onQuit, title: 'Quit Keybase'}, 117 ] 118 119 return ( 120 <> 121 <Kb.ClickableBox onClick={() => setShowingMenu(true)}> 122 <Kb.Box2 123 direction="horizontal" 124 gap="tiny" 125 centerChildren={true} 126 fullWidth={true} 127 style={styles.nameContainer} 128 alignItems="center" 129 ref={attachmentRef} 130 > 131 <Kb.Avatar 132 size={24} 133 borderColor={Styles.globalColors.blue} 134 username={username} 135 style={styles.avatar} 136 /> 137 <> 138 <Kb.Text className="username" lineClamp={1} type="BodyTinySemibold" style={styles.username}> 139 Hi {username}! 140 </Kb.Text> 141 <Kb.Icon 142 type="iconfont-arrow-down" 143 color={Styles.globalColors.blueLighter} 144 fontSize={12} 145 style={styles.caret} 146 /> 147 </> 148 </Kb.Box2> 149 </Kb.ClickableBox> 150 <Kb.FloatingMenu 151 position="bottom left" 152 containerStyle={styles.menu} 153 header={menuHeader()} 154 closeOnSelect={true} 155 visible={showingMenu} 156 attachTo={getAttachmentRef} 157 items={menuItems()} 158 onHidden={() => setShowingMenu(false)} 159 /> 160 </> 161 ) 162} 163 164const keysMap = Tabs.desktopTabOrder.reduce((map, tab, index) => { 165 map[`mod+${index + 1}`] = tab 166 return map 167}, {}) 168const hotKeys = Object.keys(keysMap) 169 170const TabBar = (props: Props) => { 171 const {selectedTab, navigation} = props 172 const username = Container.useSelector(state => state.config.username) 173 const badgeNumbers = Container.useSelector(state => state.notifications.navBadges) 174 const fsCriticalUpdate = Container.useSelector(state => state.fs.criticalUpdate) 175 176 const navRef = React.useRef(navigation.navigate) 177 178 const onChangeTab = React.useCallback((tab: Tabs.AppTab) => { 179 navRef.current(tab) 180 }, []) 181 const onNavUp = React.useCallback((tab: Tabs.AppTab) => { 182 navRef.current(tabRoots[tab]) 183 }, []) 184 const onHotKey = React.useCallback((cmd: string) => { 185 navRef.current(keysMap[cmd]) 186 }, []) 187 188 return username ? ( 189 <Kb.Box2 className="tab-container" direction="vertical" fullHeight={true}> 190 <Kb.Box2 direction="vertical" style={styles.header} fullWidth={true}> 191 <Kb.HotKey hotKeys={hotKeys} onHotKey={onHotKey} /> 192 <Kb.Box2 direction="horizontal" style={styles.osButtons} fullWidth={true} /> 193 <Header /> 194 <Kb.Divider style={styles.divider} /> 195 </Kb.Box2> 196 {tabs.map((t, i) => ( 197 <Tab 198 key={t} 199 tab={t} 200 index={i} 201 isSelected={selectedTab === t} 202 onTabClick={selectedTab === t ? onNavUp : onChangeTab} 203 badge={t === Tabs.fsTab && fsCriticalUpdate ? (badgeNumbers.get(t) ?? 0) + 1 : badgeNumbers.get(t)} 204 /> 205 ))} 206 <RuntimeStats /> 207 {flags.inviteFriends && <InviteFriends />} 208 </Kb.Box2> 209 ) : null 210} 211 212type TabProps = { 213 tab: Tabs.AppTab 214 index: number 215 isSelected: boolean 216 onTabClick: (t: Tabs.AppTab) => void 217 badge?: number 218} 219 220const Tab = React.memo((props: TabProps) => { 221 const {tab, index, isSelected, onTabClick, badge} = props 222 const {label} = data[tab] 223 224 const dispatch = Container.useDispatch() 225 226 const accountRows = Container.useSelector(state => state.config.configuredAccounts) 227 const current = Container.useSelector(state => state.config.username) 228 const onQuickSwitch = React.useMemo( 229 () => 230 index === 0 231 ? () => { 232 const row = accountRows.find(a => a.username !== current && a.hasStoredSecret) 233 if (row) { 234 dispatch(ConfigGen.createSetUserSwitching({userSwitching: true})) 235 dispatch(LoginGen.createLogin({password: new HiddenString(''), username: row.username})) 236 } else { 237 onTabClick(tab) 238 } 239 } 240 : undefined, 241 [accountRows, dispatch, index, current, onTabClick, tab] 242 ) 243 244 // no long press on desktop so a quick version 245 const [mouseTime, setMouseTime] = React.useState(0) 246 const onMouseUp = React.useMemo( 247 () => 248 index === 0 249 ? () => { 250 if (mouseTime && Date.now() - mouseTime > 1000) { 251 onQuickSwitch?.() 252 } 253 setMouseTime(0) 254 } 255 : undefined, 256 [index, onQuickSwitch, mouseTime] 257 ) 258 const onMouseDown = React.useMemo( 259 () => 260 index === 0 261 ? () => { 262 setMouseTime(Date.now()) 263 } 264 : undefined, 265 [index] 266 ) 267 const onMouseLeave = React.useMemo( 268 () => 269 index === 0 270 ? () => { 271 setMouseTime(0) 272 } 273 : undefined, 274 [index] 275 ) 276 277 return ( 278 <Kb.ClickableBox 279 feedback={false} 280 key={tab} 281 onClick={() => onTabClick(tab)} 282 onMouseDown={onMouseDown} 283 onMouseUp={onMouseUp} 284 onMouseLeave={onMouseLeave} 285 > 286 <Kb.WithTooltip 287 tooltip={`${label} (${Platforms.shortcutSymbol}${index + 1})`} 288 toastClassName="tab-tooltip" 289 > 290 <Kb.Box2 291 direction="horizontal" 292 fullWidth={true} 293 className={isSelected ? 'tab-selected' : 'tab'} 294 style={styles.tab} 295 > 296 <Kb.Box2 className="tab-highlight" direction="vertical" fullHeight={true} /> 297 <Kb.Box2 style={styles.iconBox} direction="horizontal"> 298 <Kb.Icon className="tab-icon" type={data[tab].icon} sizeType="Big" /> 299 {tab === Tabs.fsTab && <FilesTabBadge />} 300 </Kb.Box2> 301 <Kb.Text className="tab-label" type="BodySmallSemibold"> 302 {label} 303 </Kb.Text> 304 {!!badge && <Kb.Badge className="tab-badge" badgeNumber={badge} />} 305 </Kb.Box2> 306 </Kb.WithTooltip> 307 </Kb.ClickableBox> 308 ) 309}) 310 311const styles = Styles.styleSheetCreate( 312 () => 313 ({ 314 avatar: {marginLeft: 14}, 315 badgeIcon: { 316 bottom: -4, 317 position: 'absolute', 318 right: 8, 319 }, 320 badgeIconUpload: { 321 bottom: -Styles.globalMargins.xxtiny, 322 height: Styles.globalMargins.xsmall, 323 position: 'absolute', 324 right: Styles.globalMargins.xsmall, 325 width: Styles.globalMargins.xsmall, 326 }, 327 button: { 328 margin: Styles.globalMargins.xsmall, 329 }, 330 caret: {marginRight: 12}, 331 divider: {marginTop: Styles.globalMargins.tiny}, 332 fullname: {maxWidth: 180}, 333 header: {flexShrink: 0, height: 80, marginBottom: 20}, 334 headerBox: { 335 paddingTop: Styles.globalMargins.small, 336 }, 337 iconBox: { 338 justifyContent: 'flex-end', 339 position: 'relative', 340 }, 341 menu: {marginLeft: Styles.globalMargins.tiny}, 342 nameContainer: {height: 24}, 343 osButtons: Styles.platformStyles({ 344 isElectron: { 345 ...Styles.desktopStyles.windowDragging, 346 flexGrow: 1, 347 }, 348 }), 349 tab: { 350 alignItems: 'center', 351 paddingRight: 12, 352 position: 'relative', 353 }, 354 username: Styles.platformStyles({ 355 isElectron: {color: Styles.globalColors.blueLighter, flexGrow: 1, wordBreak: 'break-all'}, 356 }), 357 } as const) 358) 359 360export default TabBar 361