Dwayne Harris 4 years ago
parent
commit
827c620ca0
  1. 59
      src/actions/apps.ts
  2. 6
      src/actions/authentication.ts
  3. 40
      src/actions/groups.ts
  4. 18
      src/components/app.tsx
  5. 12
      src/components/composer.tsx
  6. 5
      src/components/create-group-step.tsx
  7. 4
      src/components/create-user-step.tsx
  8. 4
      src/components/group-invitations.tsx
  9. 3
      src/components/group-logs.tsx
  10. 4
      src/components/navigation-menu.tsx
  11. 3
      src/components/notification-container.tsx
  12. 22
      src/components/pages/about.tsx
  13. 67
      src/components/pages/admin-apps.tsx
  14. 71
      src/components/pages/admin-groups.tsx
  15. 43
      src/components/pages/admin.tsx
  16. 4
      src/components/pages/apps.tsx
  17. 4
      src/components/pages/create-app.tsx
  18. 4
      src/components/pages/developers.tsx
  19. 6
      src/components/pages/edit-app.tsx
  20. 3
      src/components/pages/group-admin.tsx
  21. 4
      src/components/pages/groups.tsx
  22. 4
      src/components/pages/home.tsx
  23. 6
      src/components/pages/login.tsx
  24. 4
      src/components/pages/register-group.tsx
  25. 6
      src/components/pages/register.tsx
  26. 5
      src/components/pages/self.tsx
  27. 6
      src/components/pages/view-app.tsx
  28. 6
      src/components/pages/view-group.tsx
  29. 4
      src/components/pages/view-post.tsx
  30. 8
      src/components/pages/view-user.tsx
  31. 3
      src/components/self-info.tsx
  32. 6
      src/components/timeline.tsx
  33. 11
      src/hooks/index.ts
  34. 7
      src/selectors/apps.ts
  35. 8
      src/selectors/groups.ts
  36. 6
      src/styles/app.css
  37. 8
      src/types/store.ts
  38. 10
      src/utils/index.ts

59
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<FetchPendingAppsResponse>({
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
}
}

6
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<Entity>({
@ -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
}
}

40
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<FetchPendingGroupsResponse>({
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
}
}

18
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<AppState, User | undefined>(getAuthenticatedUser)
const fetching = useSelector<AppState, boolean>(getFetching)
const user = useSelector(getAuthenticatedUser)
const fetching = useSelector(getFetching)
const dispatch = useDispatch<AppThunkDispatch>()
const init = async () => {
@ -129,6 +132,15 @@ const App: FC = () => {
<Route path="/about">
<About />
</Route>
<Route path="/admin/apps">
<AdminApps />
</Route>
<Route path="/admin/groups">
<AdminGroups />
</Route>
<Route path="/admin">
<Admin />
</Route>
<Route path="/">
<Home />
</Route>

12
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<Props> = ({ parent, onPost }) => {
const theme = useTheme()
const colorScheme = useSelector<AppState, string>(getColorScheme)
const installations = useSelector<AppState, Installation[]>(getInstallations)
const installation = useSelector<AppState, Installation | undefined>(getSelectedInstallation)
const height = useSelector<AppState, number>(getComposerHeight)
const error = useSelector<AppState, string | undefined>(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<AppThunkDispatch>()
const ref = useRef<HTMLIFrameElement>(null)

5
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<Props> = ({ register }) => {
const form = useSelector<AppState, Form>(getForm)
const form = useSelector(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
const next = () => {

4
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<AppState, Form>(getForm)
const form = useSelector(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
const next = () => {

4
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<Props> = ({ group }) => {
const theme = useTheme()
const invitations = useSelector<AppState, Invitation[]>(getInvitations)
const invitations = useSelector(getInvitations)
const expiration = useSelector<AppState, string>(state => getFieldValue<string>(state, 'expiration', '0'))
const limit = useSelector<AppState, string>(state => getFieldValue<string>(state, 'limit', '0'))

3
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<Props> = ({ group }) => {
const theme = useTheme()
const logs = useSelector<AppState, GroupLog[]>(getLogs)
const logs = useSelector(getLogs)
const dispatch = useDispatch()
useEffect(() => {

4
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<AppState, ColorScheme>(getColorScheme)
const scheme = useSelector(getColorScheme)
const dispatch = useDispatch()
const switchColorSchemeItem = () => {

3
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<AppState, INotification[]>(getNotifications)
const notifications = useSelector(getNotifications)
const dispatch = useDispatch()
const setAuto = (id: string) => {

22
src/components/pages/about.tsx

@ -15,25 +15,23 @@ const About: FC = () => {
})
return (
<div>
<Section>
<Title>About Flexor</Title>
<Section>
<Title>About Flexor</Title>
<p>Flexor is a service that lets users post stuff for their subscribers to see. Here are some things to know about how it works:</p>
<p>Flexor is a service that lets users post stuff for their subscribers to see. Here are some things to know about how it works:</p>
<Subtitle>Communities</Subtitle>
<Subtitle>Communities</Subtitle>
<p>Flexor is made up of Communities. Each account is created through one. Communities enforce their own standards of behavior.</p>
<p>Flexor is made up of Communities. Each account is created through one. Communities enforce their own standards of behavior.</p>
<p>Check out <Link style={{ color: theme.secondary }} to="/communities">the list of Communities</Link>.</p>
<p>Check out <Link style={{ color: theme.secondary }} to="/communities">the list of Communities</Link>.</p>
<Subtitle>Apps</Subtitle>
<Subtitle>Apps</Subtitle>
<p>Users post content to Flexor through apps created by other people/organizations.</p>
<p>Users post content to Flexor through apps created by other people/organizations.</p>
<p>Check out <Link style={{ color: theme.secondary }} to="/apps">the list of Apps</Link>.</p>
</Section>
</div>
<p>Check out <Link style={{ color: theme.secondary }} to="/apps">the list of Apps</Link>.</p>
</Section>
)
}

67
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<AppThunkDispatch>()
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 <Loading />
if (checked && !user.admin) history.push('/')
return (
<div>
<Section>
<Title>Pending Apps</Title>
<HorizontalRule />
</Section>
{apps.map(app => (
<div className="list-item" style={{ borderColor: theme.backgroundSecondary }} key={app.id}>
<AppListItem app={app} />
<div className="buttons">
<PrimaryButton text="Activate" />
<SecondaryButton text="Set Preinstall" />
</div>
</div>
))}
</div>
)
}
export default AdminApps

71
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<AppThunkDispatch>()
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 <Loading />
if (checked && !user.admin) history.push('/')
return (
<div>
<Section>
<Title>Pending Groups</Title>
<HorizontalRule />
</Section>
{groups.map(group => (
<div className="list-item" style={{ borderColor: theme.backgroundSecondary }} key={group.id}>
<GroupListItem group={group} />
<PrimaryButton text="Activate" onClick={() => handleClick(group.id)} />
</div>
))}
</div>
)
}
export default AdminGroups

43
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 <Loading />
if (checked && !user.admin) history.push('/')
return (
<Section>
<Title>Admin</Title>
<HorizontalRule />
<br /><br />
<Link style={{ color: theme.secondary }} to="/admin/groups">Approve Pending Groups</Link>
<br />
<Link style={{ color: theme.secondary }} to="/admin/apps">Approve Pending Apps</Link>
<br />
</Section>
)
}
export default Admin

4
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<AppState, App[]>(getApps)
const apps = useSelector(getApps)
const dispatch = useDispatch<AppThunkDispatch>()
useEffect(() => {

4
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<AppState, Form>(getForm)
const form = useSelector(getForm)
const fetching = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.CreateApp))
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()

4
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<AppState, App[]>(getCreatedApps)
const apps = useSelector(getCreatedApps)
const history = useHistory()
const dispatch = useDispatch<AppThunkDispatch>()

6
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<Params>()
const fetching = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.UpdateApp))
const app = useSelector<AppState, App | undefined>(state => getEntity<App>(state, EntityType.App, id))
const form = useSelector<AppState, Form>(getForm)
const selfId = useSelector<AppState, string | undefined>(getAuthenticatedUserId)
const form = useSelector(getForm)
const selfId = useSelector(getAuthenticatedUserId)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
const [showPrivateKey, setShowPrivateKey] = useState(false)

3
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<Params>()
const history = useHistory()
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const form = useSelector<AppState, Form>(getForm)
const form = useSelector(getForm)
const dispatch = useDispatch<AppThunkDispatch>()

4
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<AppState, Group[]>(getGroups)
const groups = useSelector(getGroups)
const history = useHistory()
const dispatch = useDispatch<AppThunkDispatch>()

4
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<AppState, boolean>(getAuthenticated)
const authenticated = useSelector(getAuthenticated)
const dispatch = useDispatch<AppThunkDispatch>()
useEffect(() => {

6
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<AppState, boolean>(getChecked)
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const checked = useSelector(getChecked)
const authenticated = useSelector(getAuthenticated)
const name = useSelector<AppState, string>(state => getFieldValue(state, 'name', ''))
const password = useSelector<AppState, string>(state => getFieldValue(state, 'password', ''))
const authenticating = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.Authenticate))

4
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<Params>()
const group = useSelector<AppState, Group | undefined>(state => getEntity(state, EntityType.Group, id))
const form = useSelector<AppState, Form>(getForm)
const form = useSelector(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()

6
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<AppState, number>(getStep)
const form = useSelector<AppState, Form>(getForm)
const stepIndex = useSelector(getStep)
const form = useSelector(getForm)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()

5
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<AppState, User | undefined>(getAuthenticatedUser)
const form = useSelector<AppState, Form>(getForm)
const user = useSelector(getAuthenticatedUser)
const form = useSelector(getForm)
useAuthenticationCheck()

6
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<Params>()
const theme = useTheme()
const app = useSelector<AppState, App | undefined>(state => getEntity<App>(state, EntityType.App, id))
const installations = useSelector<AppState, Installation[]>(getInstallations)
const selfId = useSelector<AppState, string | undefined>(getAuthenticatedUserId)
const installations = useSelector(getInstallations)
const selfId = useSelector(getAuthenticatedUserId)
const fetching = useSelector<AppState, boolean>(state => getIsFetching(state, RequestKey.InstallApp) || getIsFetching(state, RequestKey.UninstallApp))
const dispatch = useDispatch<AppThunkDispatch>()
const config = useConfig()

6
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<Params>()
const theme = useTheme()
const themeName = useSelector<AppState, string>(getThemeName)
const themeName = useSelector(getThemeName)
const [selectedThemeName] = useState(themeName)
const group = useSelector<AppState, Group | undefined>(state => getEntity<Group>(state, EntityType.Group, id))
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const authenticated = useSelector(getAuthenticated)
const dispatch = useDispatch<AppThunkDispatch>()
const config = useConfig()
const history = useHistory()

4
src/components/pages/view-post.tsx

@ -30,8 +30,8 @@ const ViewPost: FC = () => {
const post = useSelector<AppState, Post | undefined>(state => getEntity<Post>(state, EntityType.Post, id))
const parents = useSelector<AppState, Post[]>(state => getPostParents(state, id))
const replies = useSelector<AppState, Post[]>(state => getPostChildren(state, id))
const checked = useSelector<AppState, boolean>(getChecked)
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const checked = useSelector(getChecked)
const authenticated = useSelector(getAuthenticated)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()

8
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<Params>()
const theme = useTheme()
const themeName = useSelector<AppState, string>(getThemeName)
const themeName = useSelector(getThemeName)
const [selectedThemeName] = useState(themeName)
const checked = useSelector<AppState, boolean>(getChecked)
const self = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const checked = useSelector(getChecked)
const self = useSelector(getAuthenticatedUser)
const user = useSelector<AppState, User | undefined>(state => getEntity<User>(state, EntityType.User, id))
const posts = useSelector<AppState, Post[]>(state => getUserPosts(state, id))
const dispatch = useDispatch<AppThunkDispatch>()

3
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<AppState, User | undefined>(getAuthenticatedUser)
const user = useSelector(getAuthenticatedUser)
if (!user) {
return (

6
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<AppState, boolean>(getAuthenticated)
const posts = useSelector<AppState, Post[]>(getTimeline)
const authenticated = useSelector(getAuthenticated)
const posts = useSelector(getTimeline)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()

11
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<AppState, boolean>(getChecked)
const authenticated = useSelector<AppState, boolean>(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<AppState, Config>(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<AppState, Theme>(getTheme)
export const useTheme = () => useSelector(getTheme)
export function useSetting<T>(key: string): T | undefined
export function useSetting<T>(key: string, defaultValue: T): T
export function useSetting<T>(key: string, defaultValue?: T): T | undefined {
const user = useSelector<AppState, User | undefined>(getAuthenticatedUser)
const user = useSelector(getAuthenticatedUser)
if (!user || !user.settings) return defaultValue
return user.settings[key] ?? defaultValue

7
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[]
}

8
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[]
}

6
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;

8
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',
}

10
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
}

Loading…
Cancel
Save