diff --git a/src/actions/apps.ts b/src/actions/apps.ts index b976e13..f59adad 100644 --- a/src/actions/apps.ts +++ b/src/actions/apps.ts @@ -5,7 +5,7 @@ import { setFieldNotification } from 'src/actions/forms' import { listSet, listAppend } from 'src/actions/lists' import { objectToQuerystring } from 'src/utils' import { normalize } from 'src/utils/normalization' -import { EntityListKey } from 'src/types' +import { EntityListKey, Entity } from 'src/types' import { AppThunkAction, RequestKey, EntityType, App, AvailabilityResponse, NotificationType }from 'src/types' @@ -208,3 +208,60 @@ export const uninstallApp = (id: string): AppThunkAction => async dispatch => { throw err } } + +interface FetchPendingAppsResponse { + apps: Entity[] + continuation?: string +} + +export const fetchPendingApps = (continuation?: string): AppThunkAction => async dispatch => { + dispatch(startRequest(RequestKey.FetchPendingApps)) + + try { + const response = await apiFetch({ + path: `/v1/apps/pending?${objectToQuerystring({ continuation })}`, + method: 'get', + }) + + const apps = normalize(response.apps, EntityType.Group) + + dispatch(setEntities(apps.entities)) + dispatch(listSet(EntityListKey.PendingApps, apps.keys, response.continuation)) + dispatch(finishRequest(RequestKey.FetchPendingApps, true)) + } catch (err) { + dispatch(finishRequest(RequestKey.FetchPendingApps, false)) + throw err + } +} + +export const activateApp = (id: string): AppThunkAction => async dispatch => { + dispatch(startRequest(RequestKey.ActivateApp)) + + try { + await apiFetch({ + path: `/v1/app/${id}/activate`, + method: 'post', + }) + + dispatch(finishRequest(RequestKey.ActivateApp, true)) + } catch (err) { + dispatch(finishRequest(RequestKey.ActivateApp, false)) + throw err + } +} + +export const setPreinstall = (id: string): AppThunkAction => async dispatch => { + dispatch(startRequest(RequestKey.SetPreinstall)) + + try { + await apiFetch({ + path: `/v1/app/${id}/preinstall`, + method: 'post', + }) + + dispatch(finishRequest(RequestKey.SetPreinstall, true)) + } catch (err) { + dispatch(finishRequest(RequestKey.SetPreinstall, false)) + throw err + } +} diff --git a/src/actions/authentication.ts b/src/actions/authentication.ts index 38f8d10..39938ae 100644 --- a/src/actions/authentication.ts +++ b/src/actions/authentication.ts @@ -52,7 +52,7 @@ export const unauthenticate = (): UnauthenticateAction => ({ }) export const fetchSelf = (): AppThunkAction => async dispatch => { - dispatch(startRequest(RequestKey.FetchGroupAvailability)) + dispatch(startRequest(RequestKey.FetchSelf)) try { const self = await apiFetch({ @@ -65,10 +65,10 @@ export const fetchSelf = (): AppThunkAction => async dispatch => { dispatch(setUser(self.id)) dispatch(setAuthenticated(true)) - dispatch(finishRequest(RequestKey.FetchGroupAvailability, true)) + dispatch(finishRequest(RequestKey.FetchSelf, true)) } catch (err) { dispatch(setAuthenticated(false)) - dispatch(finishRequest(RequestKey.FetchGroupAvailability, false)) + dispatch(finishRequest(RequestKey.FetchSelf, false)) throw err } } diff --git a/src/actions/groups.ts b/src/actions/groups.ts index bdc5a00..fd2126f 100644 --- a/src/actions/groups.ts +++ b/src/actions/groups.ts @@ -184,3 +184,43 @@ export const updateGroup = (id: string, updates: object): AppThunkAction => asyn throw err } } + +interface FetchPendingGroupsResponse { + groups: Entity[] + continuation?: string +} + +export const fetchPendingGroups = (continuation?: string): AppThunkAction => async dispatch => { + dispatch(startRequest(RequestKey.FetchPendingGroups)) + + try { + const response = await apiFetch({ + path: `/v1/groups/pending?${objectToQuerystring({ continuation })}`, + method: 'get', + }) + + const groups = normalize(response.groups, EntityType.Group) + dispatch(setEntities(groups.entities)) + dispatch(listSet(EntityListKey.PendingGroups, groups.keys, response.continuation)) + dispatch(finishRequest(RequestKey.FetchPendingGroups, true)) + } catch (err) { + dispatch(finishRequest(RequestKey.FetchPendingGroups, false)) + throw err + } +} + +export const activateGroup = (id: string): AppThunkAction => async dispatch => { + dispatch(startRequest(RequestKey.ActivateGroup)) + + try { + await apiFetch({ + path: `/v1/group/${id}/activate`, + method: 'post', + }) + + dispatch(finishRequest(RequestKey.ActivateGroup, true)) + } catch (err) { + dispatch(finishRequest(RequestKey.ActivateGroup, false)) + throw err + } +} \ No newline at end of file diff --git a/src/components/app.tsx b/src/components/app.tsx index 4d32032..c8d94f0 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -12,7 +12,7 @@ import { getFetching } from 'src/selectors' import getConfig from 'src/config' import { LOCAL_STORAGE_ACCESS_TOKEN_KEY } from 'src/constants' import { useDeepCompareEffect, useTheme } from 'src/hooks' -import { AppState, AppThunkDispatch, User } from 'src/types' +import { AppThunkDispatch } from 'src/types' import Footer from './footer' import Logo from './logo' @@ -23,6 +23,9 @@ import SelfInfo from './self-info' import Spinner from './spinner' import About from './pages/about' +import Admin from './pages/admin' +import AdminApps from './pages/admin-apps' +import AdminGroups from './pages/admin-groups' import Apps from './pages/apps' import CreateApp from './pages/create-app' import Developers from './pages/developers' @@ -43,8 +46,8 @@ import '../styles/app.css' const App: FC = () => { const theme = useTheme() - const user = useSelector(getAuthenticatedUser) - const fetching = useSelector(getFetching) + const user = useSelector(getAuthenticatedUser) + const fetching = useSelector(getFetching) const dispatch = useDispatch() const init = async () => { @@ -129,6 +132,15 @@ const App: FC = () => { + + + + + + + + + diff --git a/src/components/composer.tsx b/src/components/composer.tsx index b79b676..2389233 100644 --- a/src/components/composer.tsx +++ b/src/components/composer.tsx @@ -14,7 +14,7 @@ import { showNotification } from 'src/actions/notifications' import { createPost } from 'src/actions/posts' import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from 'src/selectors/composer' import { getColorScheme } from 'src/selectors/theme' -import { AppState, Installation, AppThunkDispatch, NotificationType, Post } from 'src/types' +import { AppThunkDispatch, NotificationType, Post } from 'src/types' import { IncomingMessageData, OutgoingMessageData } from 'src/types/communicator' interface LimiterCollection { @@ -28,11 +28,11 @@ interface Props { const Composer: FC = ({ parent, onPost }) => { const theme = useTheme() - const colorScheme = useSelector(getColorScheme) - const installations = useSelector(getInstallations) - const installation = useSelector(getSelectedInstallation) - const height = useSelector(getComposerHeight) - const error = useSelector(getError) + const colorScheme = useSelector(getColorScheme) + const installations = useSelector(getInstallations) + const installation = useSelector(getSelectedInstallation) + const height = useSelector(getComposerHeight) + const error = useSelector(getError) const config = useConfig() const dispatch = useDispatch() const ref = useRef(null) diff --git a/src/components/create-group-step.tsx b/src/components/create-group-step.tsx index 08f4cfc..ac1cb68 100644 --- a/src/components/create-group-step.tsx +++ b/src/components/create-group-step.tsx @@ -1,6 +1,5 @@ import React, { FC } from 'react' import { useSelector, useDispatch } from 'react-redux' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faArrowLeft } from '@fortawesome/free-solid-svg-icons' import { setFieldNotification } from 'src/actions/forms' @@ -10,7 +9,7 @@ import { getForm } from 'src/selectors/forms' import { valueFromForm } from 'src/utils' import { MAX_ID_LENGTH } from 'src/constants' -import { AppState, AppThunkDispatch, NotificationType, Form } from 'src/types' +import { AppThunkDispatch, NotificationType } from 'src/types' import CreateGroupForm from './create-group-form' import HorizontalRule from 'src/components/horizontal-rule' @@ -22,7 +21,7 @@ interface Props { } const CreateGroupStep: FC = ({ register }) => { - const form = useSelector(getForm) + const form = useSelector(getForm) const dispatch = useDispatch() const next = () => { diff --git a/src/components/create-user-step.tsx b/src/components/create-user-step.tsx index eefa29b..8732b2c 100644 --- a/src/components/create-user-step.tsx +++ b/src/components/create-user-step.tsx @@ -9,14 +9,14 @@ import { getForm } from 'src/selectors/forms' import { MAX_ID_LENGTH, MAX_NAME_LENGTH } from 'src/constants' import { valueFromForm } from 'src/utils' -import { AppState, AppThunkDispatch, NotificationType, Form } from 'src/types' +import { AppThunkDispatch, NotificationType } from 'src/types' import CreateUserForm from './create-user-form' import HorizontalRule from 'src/components/horizontal-rule' import PrimaryButton from 'src/components/controls/primary-button' const CreateUserStep: FC = () => { - const form = useSelector(getForm) + const form = useSelector(getForm) const dispatch = useDispatch() const next = () => { diff --git a/src/components/group-invitations.tsx b/src/components/group-invitations.tsx index efe6652..77f4554 100644 --- a/src/components/group-invitations.tsx +++ b/src/components/group-invitations.tsx @@ -10,7 +10,7 @@ import { fetchInvitations, createInvitation } from 'src/actions/groups' import { getInvitations } from 'src/selectors/groups' import { getFieldValue } from 'src/selectors/forms' -import { AppState, Invitation, AppThunkDispatch } from 'src/types' +import { AppState, AppThunkDispatch } from 'src/types' import PrimaryButton from 'src/components/controls/primary-button' import Subtitle from 'src/components/subtitle' @@ -22,7 +22,7 @@ interface Props { const GroupInvitations: FC = ({ group }) => { const theme = useTheme() - const invitations = useSelector(getInvitations) + const invitations = useSelector(getInvitations) const expiration = useSelector(state => getFieldValue(state, 'expiration', '0')) const limit = useSelector(state => getFieldValue(state, 'limit', '0')) diff --git a/src/components/group-logs.tsx b/src/components/group-logs.tsx index 4850ae0..00c6c99 100644 --- a/src/components/group-logs.tsx +++ b/src/components/group-logs.tsx @@ -7,7 +7,6 @@ import { handleApiError } from 'src/api/errors' import { fetchLogs } from 'src/actions/groups' import { getLogs } from 'src/selectors/groups' import { useTheme } from 'src/hooks' -import { AppState, GroupLog } from 'src/types' interface Props { group: string @@ -15,7 +14,7 @@ interface Props { const MemberList: FC = ({ group }) => { const theme = useTheme() - const logs = useSelector(getLogs) + const logs = useSelector(getLogs) const dispatch = useDispatch() useEffect(() => { diff --git a/src/components/navigation-menu.tsx b/src/components/navigation-menu.tsx index 3660a18..676dcf8 100644 --- a/src/components/navigation-menu.tsx +++ b/src/components/navigation-menu.tsx @@ -6,11 +6,11 @@ import { faStream, faPaperPlane, faSun, faMoon } from '@fortawesome/free-solid-s import { useTheme } from 'src/hooks' import { setColorScheme } from 'src/actions/theme' import { getColorScheme } from 'src/selectors/theme' -import { AppState, ColorScheme } from 'src/types' +import { ColorScheme } from 'src/types' const NavigationMenu: FC = () => { const theme = useTheme() - const scheme = useSelector(getColorScheme) + const scheme = useSelector(getColorScheme) const dispatch = useDispatch() const switchColorSchemeItem = () => { diff --git a/src/components/notification-container.tsx b/src/components/notification-container.tsx index 2f9f741..c983d31 100644 --- a/src/components/notification-container.tsx +++ b/src/components/notification-container.tsx @@ -5,12 +5,11 @@ import { faDoorOpen } from '@fortawesome/free-solid-svg-icons' import { setNotificationAuto, removeNotification } from 'src/actions/notifications' import { getNotifications } from 'src/selectors' -import { AppState, Notification as INotification } from 'src/types' import Notification from './notification' const NotificationContainer: FC = () => { - const notifications = useSelector(getNotifications) + const notifications = useSelector(getNotifications) const dispatch = useDispatch() const setAuto = (id: string) => { diff --git a/src/components/pages/about.tsx b/src/components/pages/about.tsx index bf522d5..12663b6 100644 --- a/src/components/pages/about.tsx +++ b/src/components/pages/about.tsx @@ -15,25 +15,23 @@ const About: FC = () => { }) return ( -
-
- About Flexor +
+ About Flexor -

Flexor is a service that lets users post stuff for their subscribers to see. Here are some things to know about how it works:

+

Flexor is a service that lets users post stuff for their subscribers to see. Here are some things to know about how it works:

- Communities + Communities -

Flexor is made up of Communities. Each account is created through one. Communities enforce their own standards of behavior.

+

Flexor is made up of Communities. Each account is created through one. Communities enforce their own standards of behavior.

-

Check out the list of Communities.

+

Check out the list of Communities.

- Apps + Apps -

Users post content to Flexor through apps created by other people/organizations.

+

Users post content to Flexor through apps created by other people/organizations.

-

Check out the list of Apps.

-
-
+

Check out the list of Apps.

+ ) } diff --git a/src/components/pages/admin-apps.tsx b/src/components/pages/admin-apps.tsx new file mode 100644 index 0000000..17484a7 --- /dev/null +++ b/src/components/pages/admin-apps.tsx @@ -0,0 +1,67 @@ +import React, { FC, useEffect } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useHistory } from 'react-router-dom' +import { useAuthenticationCheck, useTheme } from 'src/hooks' +import { fetchPendingApps } from 'src/actions/apps' +import { getAuthenticatedUser, getChecked } from 'src/selectors/authentication' +import { getPendingApps } from 'src/selectors/apps' +import { handleApiError } from 'src/api/errors' +import { setTitle } from 'src/utils' +import { AppThunkDispatch } from 'src/types' + +import Section from 'src/components/section' +import Title from 'src/components/title' +import Loading from 'src/components/pages/loading' +import AppListItem from 'src/components/app-list-item' +import HorizontalRule from 'src/components/horizontal-rule' +import PrimaryButton from 'src/components/controls/primary-button' +import SecondaryButton from 'src/components/controls/secondary-button' + +const AdminApps: FC = () => { + useAuthenticationCheck() + const theme = useTheme() + const checked = useSelector(getChecked) + const user = useSelector(getAuthenticatedUser) + const apps = useSelector(getPendingApps) + const dispatch = useDispatch() + const history = useHistory() + + useEffect(() => { + setTitle('Admin \\ Apps') + + const init = async () => { + try { + await dispatch(fetchPendingApps()) + } catch (err) { + handleApiError(err, dispatch, history) + } + } + + if (checked) init() + }, [checked]) + + if (!user) return + if (checked && !user.admin) history.push('/') + + return ( +
+
+ Pending Apps + +
+ + {apps.map(app => ( +
+ + +
+ + +
+
+ ))} +
+ ) +} + +export default AdminApps diff --git a/src/components/pages/admin-groups.tsx b/src/components/pages/admin-groups.tsx new file mode 100644 index 0000000..fdac219 --- /dev/null +++ b/src/components/pages/admin-groups.tsx @@ -0,0 +1,71 @@ +import React, { FC, useEffect } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useHistory } from 'react-router-dom' +import { useAuthenticationCheck, useTheme } from 'src/hooks' +import { fetchPendingGroups, activateGroup } from 'src/actions/groups' +import { getAuthenticatedUser, getChecked } from 'src/selectors/authentication' +import { getPendingGroups } from 'src/selectors/groups' +import { handleApiError } from 'src/api/errors' +import { setTitle } from 'src/utils' +import { AppThunkDispatch } from 'src/types' + +import Section from 'src/components/section' +import Title from 'src/components/title' +import Loading from 'src/components/pages/loading' +import GroupListItem from 'src/components/group-list-item' +import HorizontalRule from 'src/components//horizontal-rule' +import PrimaryButton from 'src/components/controls/primary-button' + +const AdminGroups: FC = () => { + useAuthenticationCheck() + const theme = useTheme() + const checked = useSelector(getChecked) + const user = useSelector(getAuthenticatedUser) + const groups = useSelector(getPendingGroups) + const dispatch = useDispatch() + const history = useHistory() + + const handleClick = async (id: string) => { + try { + await dispatch(activateGroup(id)) + await dispatch(fetchPendingGroups()) + } catch (err) { + handleApiError(err, dispatch, history) + } + } + + useEffect(() => { + setTitle('Admin \\ Groups') + + const init = async () => { + try { + await dispatch(fetchPendingGroups()) + } catch (err) { + handleApiError(err, dispatch, history) + } + } + + if (checked) init() + }, [checked]) + + if (!user) return + if (checked && !user.admin) history.push('/') + + return ( +
+
+ Pending Groups + +
+ + {groups.map(group => ( +
+ + handleClick(group.id)} /> +
+ ))} +
+ ) +} + +export default AdminGroups diff --git a/src/components/pages/admin.tsx b/src/components/pages/admin.tsx new file mode 100644 index 0000000..fe90eeb --- /dev/null +++ b/src/components/pages/admin.tsx @@ -0,0 +1,43 @@ +import React, { FC, useEffect } from 'react' +import { useSelector } from 'react-redux' +import { Link, useHistory } from 'react-router-dom' +import { useTheme, useAuthenticationCheck } from 'src/hooks' +import { getAuthenticatedUser, getChecked } from 'src/selectors/authentication' +import { setTitle } from 'src/utils' + +import Section from 'src/components/section' +import Title from 'src/components/title' +import HorizontalRule from 'src/components/horizontal-rule' +import Loading from 'src/components/pages/loading' + +const Admin: FC = () => { + useAuthenticationCheck() + const checked = useSelector(getChecked) + const theme = useTheme() + const user = useSelector(getAuthenticatedUser) + const history = useHistory() + + useEffect(() => { + setTitle('Admin') + }) + + if (!user) return + if (checked && !user.admin) history.push('/') + + return ( +
+ Admin + + +

+ + Approve Pending Groups +
+ + Approve Pending Apps +
+
+ ) +} + +export default Admin diff --git a/src/components/pages/apps.tsx b/src/components/pages/apps.tsx index 6666da7..a81e221 100644 --- a/src/components/pages/apps.tsx +++ b/src/components/pages/apps.tsx @@ -5,7 +5,7 @@ import { fetchApps } from 'src/actions/apps' import { getApps } from 'src/selectors/apps' import { useTheme } from 'src/hooks' import { setTitle } from 'src/utils' -import { AppState, AppThunkDispatch, App } from 'src/types' +import { AppThunkDispatch } from 'src/types' import Title from 'src/components/title' import Section from 'src/components/section' @@ -14,7 +14,7 @@ import AppListItem from 'src/components/app-list-item' const Apps: FC = () => { const theme = useTheme() - const apps = useSelector(getApps) + const apps = useSelector(getApps) const dispatch = useDispatch() useEffect(() => { diff --git a/src/components/pages/create-app.tsx b/src/components/pages/create-app.tsx index eeebf6a..7e95226 100644 --- a/src/components/pages/create-app.tsx +++ b/src/components/pages/create-app.tsx @@ -10,7 +10,7 @@ import { getForm } from 'src/selectors/forms' import { getIsFetching } from 'src/selectors/requests' import { useTheme } from 'src/hooks' import { setTitle, valueFromForm } from 'src/utils' -import { AppState, Form, NotificationType, AppThunkDispatch, RequestKey } from 'src/types' +import { AppState, NotificationType, AppThunkDispatch, RequestKey } from 'src/types' import Title from 'src/components/title' import Section from 'src/components/section' @@ -25,7 +25,7 @@ import IconImageField from 'src/components/controls/icon-image-field' const CreateApp: FC = () => { const theme = useTheme() - const form = useSelector(getForm) + const form = useSelector(getForm) const fetching = useSelector(state => getIsFetching(state, RequestKey.CreateApp)) const dispatch = useDispatch() const history = useHistory() diff --git a/src/components/pages/developers.tsx b/src/components/pages/developers.tsx index 1137f2b..93c27ee 100644 --- a/src/components/pages/developers.tsx +++ b/src/components/pages/developers.tsx @@ -6,7 +6,7 @@ import { faPlusCircle } from '@fortawesome/free-solid-svg-icons' import { fetchCreatedApps } from 'src/actions/apps' import { getCreatedApps } from 'src/selectors/apps' import { setTitle } from 'src/utils' -import { AppState, App, AppThunkDispatch } from 'src/types' +import { AppThunkDispatch } from 'src/types' import Title from 'src/components/title' import Subtitle from 'src/components/subtitle' @@ -15,7 +15,7 @@ import HorizontalRule from 'src/components/horizontal-rule' import PrimaryButton from 'src/components/controls/primary-button' const Developers: FC = () => { - const apps = useSelector(getCreatedApps) + const apps = useSelector(getCreatedApps) const history = useHistory() const dispatch = useDispatch() diff --git a/src/components/pages/edit-app.tsx b/src/components/pages/edit-app.tsx index 5193271..175a6d6 100644 --- a/src/components/pages/edit-app.tsx +++ b/src/components/pages/edit-app.tsx @@ -14,7 +14,7 @@ import { getIsFetching } from 'src/selectors/requests' import { useAuthenticationCheck, useDeepCompareEffect, useTheme } from 'src/hooks' import { setTitle, valueFromForm } from 'src/utils' -import { AppState, AppThunkDispatch, EntityType, App, Form, NotificationType, RequestKey } from 'src/types' +import { AppState, AppThunkDispatch, EntityType, App, NotificationType, RequestKey } from 'src/types' import Title from 'src/components/title' import Section from 'src/components/section' @@ -39,8 +39,8 @@ const EditApp: FC = () => { const { id } = useParams() const fetching = useSelector(state => getIsFetching(state, RequestKey.UpdateApp)) const app = useSelector(state => getEntity(state, EntityType.App, id)) - const form = useSelector(getForm) - const selfId = useSelector(getAuthenticatedUserId) + const form = useSelector(getForm) + const selfId = useSelector(getAuthenticatedUserId) const dispatch = useDispatch() const history = useHistory() const [showPrivateKey, setShowPrivateKey] = useState(false) diff --git a/src/components/pages/group-admin.tsx b/src/components/pages/group-admin.tsx index 8caca69..e8a205b 100644 --- a/src/components/pages/group-admin.tsx +++ b/src/components/pages/group-admin.tsx @@ -18,7 +18,6 @@ import { GroupMembershipType, Tab, EntityType, - Form, } from 'src/types' import Title from 'src/components/title' @@ -53,7 +52,7 @@ const GroupAdmin: FC = () => { const { id, tab = '' } = useParams() const history = useHistory() const group = useSelector(state => getEntity(state, EntityType.Group, id)) - const form = useSelector(getForm) + const form = useSelector(getForm) const dispatch = useDispatch() diff --git a/src/components/pages/groups.tsx b/src/components/pages/groups.tsx index fe3c6f4..766a362 100644 --- a/src/components/pages/groups.tsx +++ b/src/components/pages/groups.tsx @@ -6,7 +6,7 @@ import { faPlusCircle } from '@fortawesome/free-solid-svg-icons' import { fetchGroups } from 'src/actions/groups' import { getGroups } from 'src/selectors/groups' import { setTitle } from 'src/utils' -import { AppState, AppThunkDispatch, Group } from 'src/types' +import { AppThunkDispatch } from 'src/types' import Title from 'src/components/title' import Section from 'src/components/section' @@ -15,7 +15,7 @@ import GroupListItem from 'src/components/group-list-item' import PrimaryButton from 'src/components/controls/primary-button' const Groups: FC = () => { - const groups = useSelector(getGroups) + const groups = useSelector(getGroups) const history = useHistory() const dispatch = useDispatch() diff --git a/src/components/pages/home.tsx b/src/components/pages/home.tsx index e710ec3..4420330 100644 --- a/src/components/pages/home.tsx +++ b/src/components/pages/home.tsx @@ -4,7 +4,7 @@ import { useSelector, useDispatch } from 'react-redux' import { fetchTimeline } from 'src/actions/posts' import { getAuthenticated } from 'src/selectors/authentication' import { setTitle } from 'src/utils' -import { AppState, AppThunkDispatch } from 'src/types' +import { AppThunkDispatch } from 'src/types' import Title from 'src/components/title' import Composer from 'src/components/composer' @@ -13,7 +13,7 @@ import Section from 'src/components/section' import Subtitle from 'src/components/subtitle' const Home: FC = () => { - const authenticated = useSelector(getAuthenticated) + const authenticated = useSelector(getAuthenticated) const dispatch = useDispatch() useEffect(() => { diff --git a/src/components/pages/login.tsx b/src/components/pages/login.tsx index ab44408..dd8970f 100644 --- a/src/components/pages/login.tsx +++ b/src/components/pages/login.tsx @@ -1,7 +1,7 @@ import React, { FC, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router' -import { faIdCard, faKey } from '@fortawesome/free-solid-svg-icons' +import { faIdCard } from '@fortawesome/free-solid-svg-icons' import { handleApiError } from 'src/api/errors' import { authenticate } from 'src/actions/authentication' @@ -20,8 +20,8 @@ import PrimaryButton from 'src/components/controls/primary-button' import { AppState, RequestKey, NotificationType } from 'src/types' const Login: FC = () => { - const checked = useSelector(getChecked) - const authenticated = useSelector(getAuthenticated) + const checked = useSelector(getChecked) + const authenticated = useSelector(getAuthenticated) const name = useSelector(state => getFieldValue(state, 'name', '')) const password = useSelector(state => getFieldValue(state, 'password', '')) const authenticating = useSelector(state => getIsFetching(state, RequestKey.Authenticate)) diff --git a/src/components/pages/register-group.tsx b/src/components/pages/register-group.tsx index 185d803..ba5cde4 100644 --- a/src/components/pages/register-group.tsx +++ b/src/components/pages/register-group.tsx @@ -14,7 +14,7 @@ import { getForm } from 'src/selectors/forms' import { setTitle, valueFromForm, getDefaultThemeName } from 'src/utils' import { useDeepCompareEffect } from 'src/hooks' -import { AppState, AppThunkDispatch, Group, EntityType, NotificationType, Form } from 'src/types' +import { AppState, AppThunkDispatch, Group, EntityType, NotificationType } from 'src/types' import Title from 'src/components/title' import Subtitle from 'src/components/subtitle' @@ -31,7 +31,7 @@ interface Params { const RegisterGroup: FC = () => { const { id } = useParams() const group = useSelector(state => getEntity(state, EntityType.Group, id)) - const form = useSelector(getForm) + const form = useSelector(getForm) const dispatch = useDispatch() const history = useHistory() diff --git a/src/components/pages/register.tsx b/src/components/pages/register.tsx index 8e950b3..ed04e33 100644 --- a/src/components/pages/register.tsx +++ b/src/components/pages/register.tsx @@ -9,7 +9,7 @@ import { initForm, initField } from 'src/actions/forms' import { showNotification } from 'src/actions/notifications' import { createGroup, register } from 'src/actions/registration' import { setTitle, valueFromForm, getDefaultThemeName } from 'src/utils' -import { AppState, AppThunkDispatch, Form, NotificationType } from 'src/types' +import { AppThunkDispatch, NotificationType } from 'src/types' import Title from 'src/components/title' import Section from 'src/components/section' @@ -18,8 +18,8 @@ import CreateGroupStep from 'src/components/create-group-step' import CreateUserStep from 'src/components/create-user-step' const Register: FC = () => { - const stepIndex = useSelector(getStep) - const form = useSelector(getForm) + const stepIndex = useSelector(getStep) + const form = useSelector(getForm) const dispatch = useDispatch() const history = useHistory() diff --git a/src/components/pages/self.tsx b/src/components/pages/self.tsx index 7bc46ef..63c3b74 100644 --- a/src/components/pages/self.tsx +++ b/src/components/pages/self.tsx @@ -12,7 +12,6 @@ import { handleApiError } from 'src/api/errors' import { PRIVACY_OPTIONS } from 'src/constants' import { useAuthenticationCheck, useDeepCompareEffect } from 'src/hooks' import { setTitle, valueFromForm } from 'src/utils' -import { AppState, User, Form } from 'src/types' import Title from 'src/components/title' import Subtitle from 'src/components/subtitle' @@ -34,8 +33,8 @@ const Self: FC = () => { const dispatch = useDispatch() const history = useHistory() - const user = useSelector(getAuthenticatedUser) - const form = useSelector(getForm) + const user = useSelector(getAuthenticatedUser) + const form = useSelector(getForm) useAuthenticationCheck() diff --git a/src/components/pages/view-app.tsx b/src/components/pages/view-app.tsx index e476b31..17cd2dc 100644 --- a/src/components/pages/view-app.tsx +++ b/src/components/pages/view-app.tsx @@ -14,7 +14,7 @@ import { getIsFetching } from 'src/selectors/requests' import { useConfig, useTheme } from 'src/hooks' import { setTitle, urlForBlob } from 'src/utils' -import { AppState, AppThunkDispatch, EntityType, App, RequestKey, Installation, LevelItem } from 'src/types' +import { AppState, AppThunkDispatch, EntityType, App, RequestKey, LevelItem } from 'src/types' import Title from 'src/components/title' import Section from 'src/components/section' @@ -31,8 +31,8 @@ const ViewApp: FC = () => { const { id } = useParams() const theme = useTheme() const app = useSelector(state => getEntity(state, EntityType.App, id)) - const installations = useSelector(getInstallations) - const selfId = useSelector(getAuthenticatedUserId) + const installations = useSelector(getInstallations) + const selfId = useSelector(getAuthenticatedUserId) const fetching = useSelector(state => getIsFetching(state, RequestKey.InstallApp) || getIsFetching(state, RequestKey.UninstallApp)) const dispatch = useDispatch() const config = useConfig() diff --git a/src/components/pages/view-group.tsx b/src/components/pages/view-group.tsx index b63cb42..43af56c 100644 --- a/src/components/pages/view-group.tsx +++ b/src/components/pages/view-group.tsx @@ -5,6 +5,7 @@ import { faEdit, faUserCheck, faBan } from '@fortawesome/free-solid-svg-icons' import moment from 'moment' import { handleApiError } from 'src/api/errors' +import { setTheme } from 'src/actions/theme' import { fetchGroup } from 'src/actions/groups' import { getAuthenticated } from 'src/selectors/authentication' import { getEntity } from 'src/selectors/entities' @@ -21,7 +22,6 @@ import PrimaryButton from 'src/components/controls/primary-button' import Button from 'src/components/controls/button' import Loading from 'src/components/pages/loading' import HorizontalRule from 'src/components/horizontal-rule' -import { setTheme } from 'src/actions/theme' interface Params { id: string @@ -30,10 +30,10 @@ interface Params { const ViewGroup: FC = () => { const { id } = useParams() const theme = useTheme() - const themeName = useSelector(getThemeName) + const themeName = useSelector(getThemeName) const [selectedThemeName] = useState(themeName) const group = useSelector(state => getEntity(state, EntityType.Group, id)) - const authenticated = useSelector(getAuthenticated) + const authenticated = useSelector(getAuthenticated) const dispatch = useDispatch() const config = useConfig() const history = useHistory() diff --git a/src/components/pages/view-post.tsx b/src/components/pages/view-post.tsx index 8cdd5f7..f5ba57f 100644 --- a/src/components/pages/view-post.tsx +++ b/src/components/pages/view-post.tsx @@ -30,8 +30,8 @@ const ViewPost: FC = () => { const post = useSelector(state => getEntity(state, EntityType.Post, id)) const parents = useSelector(state => getPostParents(state, id)) const replies = useSelector(state => getPostChildren(state, id)) - const checked = useSelector(getChecked) - const authenticated = useSelector(getAuthenticated) + const checked = useSelector(getChecked) + const authenticated = useSelector(getAuthenticated) const dispatch = useDispatch() const history = useHistory() diff --git a/src/components/pages/view-user.tsx b/src/components/pages/view-user.tsx index e751acf..7fde2cf 100644 --- a/src/components/pages/view-user.tsx +++ b/src/components/pages/view-user.tsx @@ -16,7 +16,7 @@ import { getThemeName } from 'src/selectors/theme' import { useDeepCompareEffect, useConfig, useTheme, useSetting } from 'src/hooks' import { setTitle, urlForBlob } from 'src/utils' -import { AppState, Theme, EntityType, User, Post, AppThunkDispatch, LevelItem } from 'src/types' +import { AppState, EntityType, User, Post, AppThunkDispatch, LevelItem } from 'src/types' import Title from 'src/components/title' import Subtitle from 'src/components/subtitle' @@ -33,10 +33,10 @@ interface Params { const ViewUser: FC = () => { const { id } = useParams() const theme = useTheme() - const themeName = useSelector(getThemeName) + const themeName = useSelector(getThemeName) const [selectedThemeName] = useState(themeName) - const checked = useSelector(getChecked) - const self = useSelector(getAuthenticatedUser) + const checked = useSelector(getChecked) + const self = useSelector(getAuthenticatedUser) const user = useSelector(state => getEntity(state, EntityType.User, id)) const posts = useSelector(state => getUserPosts(state, id)) const dispatch = useDispatch() diff --git a/src/components/self-info.tsx b/src/components/self-info.tsx index 764212b..5001a9c 100644 --- a/src/components/self-info.tsx +++ b/src/components/self-info.tsx @@ -4,14 +4,13 @@ import { Link } from 'react-router-dom' import { getAuthenticatedUser } from 'src/selectors/authentication' import { useConfig, useTheme } from 'src/hooks' import { urlForBlob } from 'src/utils' -import { AppState, User } from 'src/types' import HorizontalRule from 'src/components/horizontal-rule' const SelfInfo: FC = () => { const theme = useTheme() const config = useConfig() - const user = useSelector(getAuthenticatedUser) + const user = useSelector(getAuthenticatedUser) if (!user) { return ( diff --git a/src/components/timeline.tsx b/src/components/timeline.tsx index 3f438cc..854b6ea 100644 --- a/src/components/timeline.tsx +++ b/src/components/timeline.tsx @@ -7,13 +7,13 @@ import { fetchTimeline } from 'src/actions/posts' import { getTimeline } from 'src/selectors/posts' import { getAuthenticated } from 'src/selectors/authentication' -import { AppState, Post, AppThunkDispatch } from 'src/types' +import { AppThunkDispatch } from 'src/types' import PostList from 'src/components/post-list' const Timeline: FC = () => { - const authenticated = useSelector(getAuthenticated) - const posts = useSelector(getTimeline) + const authenticated = useSelector(getAuthenticated) + const posts = useSelector(getTimeline) const dispatch = useDispatch() const history = useHistory() diff --git a/src/hooks/index.ts b/src/hooks/index.ts index bacdffe..fed8bea 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -6,11 +6,10 @@ import isEqual from 'lodash/isEqual' import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication' import { getTheme } from 'src/selectors/theme' import { getConfig } from 'src/selectors' -import { AppState, Theme, Config, User } from 'src/types' export const useAuthenticationCheck = () => { - const checked = useSelector(getChecked) - const authenticated = useSelector(getAuthenticated) + const checked = useSelector(getChecked) + const authenticated = useSelector(getAuthenticated) const history = useHistory() useEffect(() => { @@ -18,7 +17,7 @@ export const useAuthenticationCheck = () => { }, [checked, authenticated]) } -export const useConfig = () => useSelector(getConfig) +export const useConfig = () => useSelector(getConfig) const useDeepCompareMemoize = (value: any) => { const ref = useRef() @@ -34,12 +33,12 @@ export const useDeepCompareEffect = (callback: EffectCallback, deps?: readonly a useEffect(callback, useDeepCompareMemoize(deps)) } -export const useTheme = () => useSelector(getTheme) +export const useTheme = () => useSelector(getTheme) export function useSetting(key: string): T | undefined export function useSetting(key: string, defaultValue: T): T export function useSetting(key: string, defaultValue?: T): T | undefined { - const user = useSelector(getAuthenticatedUser) + const user = useSelector(getAuthenticatedUser) if (!user || !user.settings) return defaultValue return user.settings[key] ?? defaultValue diff --git a/src/selectors/apps.ts b/src/selectors/apps.ts index ace94b5..2ba0a66 100644 --- a/src/selectors/apps.ts +++ b/src/selectors/apps.ts @@ -14,3 +14,10 @@ export const getCreatedApps = (state: AppState) => { return denormalize(entityList.entities, EntityType.App, state.entities) as App[] } + +export const getPendingApps = (state: AppState) => { + const entityList = state.lists[EntityListKey.PendingApps] + if (!entityList) return [] + + return denormalize(entityList.entities, EntityType.App, state.entities) as App[] +} diff --git a/src/selectors/groups.ts b/src/selectors/groups.ts index 469412b..86497b0 100644 --- a/src/selectors/groups.ts +++ b/src/selectors/groups.ts @@ -14,6 +14,7 @@ export const getLogs = (state: AppState) => { return denormalize(entityList.entities, EntityType.Log, state.entities) as GroupLog[] } + export const getInvitations = (state: AppState) => { const entityList = state.lists[EntityListKey.Invitations] if (!entityList) return [] @@ -27,3 +28,10 @@ export const getGroupMembers = (state: AppState, group: string) => { return denormalize(Object.values(users).filter(user => user.group === group).map(user => user.id), EntityType.User, state.entities) as User[] } + +export const getPendingGroups = (state: AppState) => { + const entityList = state.lists[EntityListKey.PendingGroups] + if (!entityList) return [] + + return denormalize(entityList.entities, EntityType.Group, state.entities) as Group[] +} diff --git a/src/styles/app.css b/src/styles/app.css index 80ee829..32b2cc6 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -467,6 +467,12 @@ div.header img { width: 128px; } +div.list-item { + border-bottom: var(--default-border); + margin-bottom: 1rem; + padding: 1rem; +} + div.app-list-item, div.group-list-item { display: flex; margin: 1rem 0px; diff --git a/src/types/store.ts b/src/types/store.ts index 5c8e7e4..ba8934e 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -10,6 +10,8 @@ export enum NotificationType { } export enum RequestKey { + ActivateApp = 'activate-app', + ActivateGroup = 'activate-group', Authenticate = 'authenticate', CreateApp = 'create-app', CreateGroup = 'create-group', @@ -25,7 +27,10 @@ export enum RequestKey { FetchGroups = 'fetch-groups', FetchInstallations = 'fetch-installations', FetchInvitations = 'fetch-invitations', + FetchPendingApps = 'fetch-pending-apps', + FetchPendingGroups = 'fetch-pending-groups', FetchPost = 'fetch-post', + FetchSelf = 'fetch-self', FetchSelfApps = 'fetch-self-apps', FetchTimeline = 'fetch-timeline', FetchUser = 'fetch-user', @@ -33,6 +38,7 @@ export enum RequestKey { FetchUserPosts = 'fetch-user-posts', InstallApp = 'install-app', Register = 'register', + SetPreinstall = 'set-preinstall', Subscribe = 'subscribe', UninstallApp = 'uninstall-app', Unsubscribe = 'unsubscribe', @@ -49,6 +55,8 @@ export enum EntityListKey { GroupMembers = 'group-members', Logs = 'logs', Invitations = 'invitations', + PendingApps = 'pending-apps', + PendingGroups = 'pending-groups', Timeline = 'timeline', } diff --git a/src/utils/index.ts b/src/utils/index.ts index edae197..78b0482 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,18 +1,12 @@ import getConfig from 'src/config' import themes from 'src/themes' - -import { - Form, - FormValue, - Config, - ClassDictionary, -} from 'src/types' +import { Form, FormValue, Config, ClassDictionary } from 'src/types' export const objectToQuerystring = (obj: object) => Object.entries(obj).filter(([_, value]) => value !== undefined).map(([name, value]) => `${name}=${value}`).join('&') export function setTitle(title: string, decorate: boolean = true) { if (decorate) { - document.title = `${title} \ Flexor` + document.title = `${title} \\ Flexor` } else { document.title = title }