diff --git a/web/app-list/app-list.css b/web/app-list/app-list.css
new file mode 100644
--- /dev/null
+++ b/web/app-list/app-list.css
@@ -0,0 +1,15 @@
+.container {
+  display: flex;
+  flex-direction: column;
+  min-width: 160px;
+}
+
+.appList {
+  display: flex;
+  flex-direction: column;
+  row-gap: 16px;
+  background-color: var(--card-background-primary-default);
+  padding: 16px;
+  flex: 1;
+  border-radius: 0 0 8px 8px;
+}
diff --git a/web/app-list/app-list.react.js b/web/app-list/app-list.react.js
new file mode 100644
--- /dev/null
+++ b/web/app-list/app-list.react.js
@@ -0,0 +1,96 @@
+// @flow
+
+import * as React from 'react';
+
+import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors.js';
+import { useDispatch } from 'lib/utils/redux-utils.js';
+
+import AppListHeader from './app-list-header.react.js';
+import AppListItem from './app-list-item.react.js';
+import css from './app-list.css';
+import { updateNavInfoActionType } from '../redux/action-types.js';
+import { useSelector } from '../redux/redux-utils.js';
+
+function AppList(): React.Node {
+  const dispatch = useDispatch();
+
+  const onClickCalendar = React.useCallback(() => {
+    dispatch({
+      type: updateNavInfoActionType,
+      payload: { tab: 'calendar' },
+    });
+  }, [dispatch]);
+
+  const isCalendarEnabled = useSelector(state => state.enabledApps.calendar);
+
+  const calendarAppListItem = React.useMemo(() => {
+    if (!isCalendarEnabled) {
+      return null;
+    }
+
+    return (
+      <AppListItem
+        id="calendar"
+        name="Calendar"
+        icon="calendar"
+        onClick={onClickCalendar}
+      />
+    );
+  }, [isCalendarEnabled, onClickCalendar]);
+
+  const activeChatThreadID = useSelector(
+    state => state.navInfo.activeChatThreadID,
+  );
+  const mostRecentlyReadThread = useSelector(mostRecentlyReadThreadSelector);
+  const isActiveThreadCurrentlyUnread = useSelector(
+    state =>
+      !activeChatThreadID ||
+      !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread,
+  );
+
+  const onClickInbox = React.useCallback(() => {
+    dispatch({
+      type: updateNavInfoActionType,
+      payload: {
+        tab: 'chat',
+        activeChatThreadID: isActiveThreadCurrentlyUnread
+          ? mostRecentlyReadThread
+          : activeChatThreadID,
+      },
+    });
+  }, [
+    dispatch,
+    isActiveThreadCurrentlyUnread,
+    mostRecentlyReadThread,
+    activeChatThreadID,
+  ]);
+
+  const appListBody = React.useMemo(
+    () => (
+      <div className={css.appList}>
+        <AppListItem
+          id="chat"
+          icon="message-square"
+          name="Inbox"
+          onClick={onClickInbox}
+        />
+        {calendarAppListItem}
+      </div>
+    ),
+    [calendarAppListItem, onClickInbox],
+  );
+
+  const appList = React.useMemo(
+    () => (
+      <div className={css.container}>
+        <AppListHeader />
+        {appListBody}
+      </div>
+    ),
+    [appListBody],
+  );
+
+  return appList;
+}
+
+export default AppList;