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