Dwayne Harris 4 years ago
parent
commit
25cdcc8213
  1. 6
      src/actions/authentication.ts
  2. 19
      src/actions/composer.ts
  3. 104
      src/components/app.tsx
  4. 36
      src/components/composer.tsx
  5. 2
      src/components/controls/checkbox-field.tsx
  6. 2
      src/components/pages/about.tsx
  7. 18
      src/components/pages/self.tsx
  8. 7
      src/components/pages/view-group.tsx
  9. 27
      src/components/pages/view-user.tsx
  10. 13
      src/hooks/index.ts
  11. 20
      src/styles/app.css
  12. 4
      src/types/index.ts
  13. 1
      src/types/store.ts

6
src/actions/authentication.ts

@ -11,7 +11,7 @@ import {
LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY,
} from 'src/constants'
import { AppThunkAction, Entity, RequestKey, EntityType } from 'src/types'
import { AppThunkAction, Entity, RequestKey, EntityType, Settings } from 'src/types'
export interface SetCheckedAction extends Action {
type: 'AUTHENTICATION_SET_CHECKED'
@ -115,10 +115,11 @@ interface UpdateSelfOptions {
imageUrl: string
coverImageUrl: string
theme: string
settings: Settings
}
export const updateSelf = (options: UpdateSelfOptions): AppThunkAction => async dispatch => {
const { name, about, requiresApproval, privacy, imageUrl, coverImageUrl, theme } = options
const { name, about, requiresApproval, privacy, imageUrl, coverImageUrl, theme, settings } = options
dispatch(startRequest(RequestKey.UpdateSelf))
try {
@ -133,6 +134,7 @@ export const updateSelf = (options: UpdateSelfOptions): AppThunkAction => async
imageUrl,
coverImageUrl,
theme,
settings,
},
})

19
src/actions/composer.ts

@ -4,7 +4,7 @@ import { setEntities } from 'src/actions/entities'
import { startRequest, finishRequest } from 'src/actions/requests'
import { apiFetch } from 'src/api'
import { normalize } from 'src/utils/normalization'
import { AppThunkAction, Installation, RequestKey, EntityType } from 'src/types'
import { AppThunkAction, Installation, RequestKey, EntityType, Settings } from 'src/types'
export interface SetInstallationsAction extends Action {
type: 'COMPOSER_SET_INSTALLATIONS'
@ -70,3 +70,20 @@ export const fetchInstallations = (): AppThunkAction => async dispatch => {
throw err
}
}
export const saveInstallationSettings = (id: string, settings: Settings): AppThunkAction => async dispatch => {
dispatch(startRequest(RequestKey.UpdateInstallationSettings))
try {
await apiFetch({
path: `/api/installation/${id}/settings`,
method: 'put',
body: { settings },
})
dispatch(finishRequest(RequestKey.UpdateInstallationSettings, true))
} catch (err) {
dispatch(finishRequest(RequestKey.UpdateInstallationSettings, false))
throw err
}
}

104
src/components/app.tsx

@ -81,57 +81,59 @@ const App: FC = () => {
<Logo />
</div>
<div className="content" style={{ backgroundColor: theme.backgroundPrimary, borderLeftColor: theme.backgroundSecondary, borderRightColor: theme.backgroundPrimary }}>
<Switch>
<Route path="/c/:id/admin/:tab?">
<GroupAdmin />
</Route>
<Route path="/c/:id/register">
<RegisterGroup />
</Route>
<Route path="/c/:id">
<ViewGroup />
</Route>
<Route path="/a/:id/edit">
<EditApp />
</Route>
<Route path="/a/:id">
<ViewApp />
</Route>
<Route path="/u/:id">
<ViewUser />
</Route>
<Route path="/p/:id">
<ViewPost />
</Route>
<Route path="/login">
<Login />
</Route>
<Route path="/register">
<Register />
</Route>
<Route path="/communities">
<Groups />
</Route>
<Route path="/self">
<Self />
</Route>
<Route path="/apps">
<Apps />
</Route>
<Route path="/developers/create">
<CreateApp />
</Route>
<Route path="/developers">
<Developers />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
<div className="content-container" style={{ backgroundColor: theme.backgroundPrimary }}>
<div className="content" style={{ borderLeftColor: theme.backgroundSecondary, borderRightColor: theme.backgroundPrimary }}>
<Switch>
<Route path="/c/:id/admin/:tab?">
<GroupAdmin />
</Route>
<Route path="/c/:id/register">
<RegisterGroup />
</Route>
<Route path="/c/:id">
<ViewGroup />
</Route>
<Route path="/a/:id/edit">
<EditApp />
</Route>
<Route path="/a/:id">
<ViewApp />
</Route>
<Route path="/u/:id">
<ViewUser />
</Route>
<Route path="/p/:id">
<ViewPost />
</Route>
<Route path="/login">
<Login />
</Route>
<Route path="/register">
<Register />
</Route>
<Route path="/communities">
<Groups />
</Route>
<Route path="/self">
<Self />
</Route>
<Route path="/apps">
<Apps />
</Route>
<Route path="/developers/create">
<CreateApp />
</Route>
<Route path="/developers">
<Developers />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</div>
<div className="menu-container">

36
src/components/composer.tsx

@ -3,7 +3,13 @@ import { useSelector, useDispatch } from 'react-redux'
import { getOrigin } from 'src/utils'
import { useConfig, useDeepCompareEffect, useTheme } from 'src/hooks'
import { fetchInstallations, setSelectedInstallation, setHeight as setComposerHeight, setError as setComposerError } from 'src/actions/composer'
import {
fetchInstallations,
setSelectedInstallation,
setHeight as setComposerHeight,
setError as setComposerError,
saveInstallationSettings,
} from 'src/actions/composer'
import { showNotification } from 'src/actions/notifications'
import { createPost } from 'src/actions/posts'
import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from 'src/selectors/composer'
@ -120,6 +126,28 @@ const Composer: FC<Props> = ({ parent, onPost }) => {
content: {},
})
break
case 'saveSettings':
withRateLimit(async ({ name, content }) => {
try {
await dispatch(saveInstallationSettings(installation.id, content.settings))
postMessage({
name,
content: {
settings: content.settings,
},
})
} catch (error) {
postMessage({
name,
error,
})
dispatch(showNotification(NotificationType.Error, `Error saving settings: ${error.message}`))
}
}, data, 2000)
break
case 'post':
withRateLimit(async ({ name, content }) => {
@ -143,13 +171,13 @@ const Composer: FC<Props> = ({ parent, onPost }) => {
dispatch(showNotification(NotificationType.Success, `Posted!`))
if (onPost) onPost()
} catch (err) {
} catch (error) {
postMessage({
name,
error: err,
error,
})
dispatch(showNotification(NotificationType.Error, `Error posting: ${err.message}`))
dispatch(showNotification(NotificationType.Error, `Error posting: ${error.message}`))
}
}, data, 2000)

2
src/components/controls/checkbox-field.tsx

@ -16,7 +16,7 @@ const CheckboxField: FC<Props> = ({ name, children }) => {
return (
<label className="checkbox">
<input type="checkbox" checked={value} onChange={e => dispatch(setFieldValue(name, e.target.checked))} style={{ backgroundColor: theme.primary }} />
<input type="checkbox" checked={value} onChange={e => dispatch(setFieldValue(name, e.target.checked))} style={{ color: theme.primary, border: 'none' }} />
&nbsp;&nbsp;
<span style={{ color: theme.text }}>{children}</span>
</label>

2
src/components/pages/about.tsx

@ -25,7 +25,7 @@ const About: FC = () => {
<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="/groups">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>

18
src/components/pages/self.tsx

@ -1,6 +1,6 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useHistory } from 'react-router-dom'
import { useHistory } from 'react-router-dom'
import { faDoorOpen, faCheckCircle, faIdCard, faEnvelope, faUserShield, faUserCircle } from '@fortawesome/free-solid-svg-icons'
import { unauthenticate, updateSelf } from 'src/actions/authentication'
@ -10,7 +10,7 @@ import { getForm } from 'src/selectors/forms'
import { handleApiError } from 'src/api/errors'
import { PRIVACY_OPTIONS } from 'src/constants'
import { useAuthenticationCheck, useDeepCompareEffect, useTheme } from 'src/hooks'
import { useAuthenticationCheck, useDeepCompareEffect } from 'src/hooks'
import { setTitle, valueFromForm } from 'src/utils'
import { AppState, User, Form } from 'src/types'
@ -31,7 +31,6 @@ import ThemeField from 'src/components/controls/theme-field'
import StaticField from 'src/components/controls/static-field'
const Self: FC = () => {
const theme = useTheme()
const dispatch = useDispatch()
const history = useHistory()
@ -47,6 +46,8 @@ const Self: FC = () => {
}
const handleUpdate = () => {
if (!user) return
const settings = user.settings ?? {}
const name = valueFromForm<string>(form, 'name', '')
const about = valueFromForm<string>(form, 'about', '')
const requiresApproval = valueFromForm<boolean>(form, 'requiresApproval', true)
@ -54,6 +55,7 @@ const Self: FC = () => {
const imageUrl = valueFromForm<string>(form, 'image', '')
const coverImageUrl = valueFromForm<string>(form, 'coverImage', '')
const theme = valueFromForm<string>(form, 'theme', '')
const allowThemeChange = valueFromForm<boolean>(form, 'allowThemeChange', true)
try {
dispatch(updateSelf({
@ -64,6 +66,10 @@ const Self: FC = () => {
imageUrl,
coverImageUrl,
theme,
settings: {
...settings,
allowThemeChange,
}
}))
} catch (err) {
handleApiError(err, dispatch, history)
@ -82,6 +88,7 @@ const Self: FC = () => {
dispatch(initField('image', user.imageUrl || ''))
dispatch(initField('coverImage', user.coverImageUrl || ''))
dispatch(initField('theme', user.theme))
dispatch(initField('allowThemeChange', user.settings.allowThemeChange))
}
}, [user])
@ -108,6 +115,11 @@ const Self: FC = () => {
<CheckboxField name="requiresApproval">
Approve each Subscription request from other users.
</CheckboxField>
<br />
<CheckboxField name="allowThemeChange">
Allow theme changes on User and Community pages.
</CheckboxField>
<HorizontalRule />

7
src/components/pages/view-group.tsx

@ -10,7 +10,7 @@ import { getAuthenticated } from 'src/selectors/authentication'
import { getEntity } from 'src/selectors/entities'
import { getThemeName } from 'src/selectors/theme'
import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks'
import { useDeepCompareEffect, useConfig, useTheme, useSetting } from 'src/hooks'
import { setTitle, urlForBlob } from 'src/utils'
import { AppState, EntityType, Group, GroupMembershipType, AppThunkDispatch, LevelItem } from 'src/types'
@ -37,6 +37,7 @@ const ViewGroup: FC = () => {
const dispatch = useDispatch<AppThunkDispatch>()
const config = useConfig()
const history = useHistory()
const allowThemeChange = useSetting<boolean>('allowThemeChange', true)
useEffect(() => {
try {
@ -49,11 +50,11 @@ const ViewGroup: FC = () => {
useDeepCompareEffect(() => {
if (group) {
setTitle(group.name)
dispatch(setTheme(group.theme))
if (allowThemeChange) dispatch(setTheme(group.theme))
}
return () => {
dispatch(setTheme(selectedThemeName))
if (allowThemeChange) dispatch(setTheme(selectedThemeName))
}
}, [group])

27
src/components/pages/view-user.tsx

@ -14,7 +14,7 @@ import { getAuthenticatedUser, getChecked } from 'src/selectors/authentication'
import { getUserPosts } from 'src/selectors/posts'
import { getThemeName } from 'src/selectors/theme'
import { useDeepCompareEffect, useConfig, useTheme } from 'src/hooks'
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'
@ -42,6 +42,7 @@ const ViewUser: FC = () => {
const dispatch = useDispatch<AppThunkDispatch>()
const config = useConfig()
const history = useHistory()
const allowThemeChange = useSetting<boolean>('allowThemeChange', true)
useEffect(() => {
const init = async () => {
@ -59,11 +60,11 @@ const ViewUser: FC = () => {
useDeepCompareEffect(() => {
if (user) {
setTitle(user.name)
dispatch(setTheme(user.theme))
if (allowThemeChange && user.theme) dispatch(setTheme(user.theme))
}
return () => {
if (selectedThemeName) dispatch(setTheme(selectedThemeName))
if (allowThemeChange && selectedThemeName) dispatch(setTheme(selectedThemeName))
}
}, [user])
@ -121,8 +122,8 @@ const ViewUser: FC = () => {
<div className="buttons">
{subscribed &&
<button className="button is-danger" onClick={() => dispatch(unsubscribe(user.id))}>
<span className="icon is-small">
<button style={{ backgroundColor: theme.red, color: 'white' }} onClick={() => dispatch(unsubscribe(user.id))}>
<span className="icon">
<FontAwesomeIcon icon={faUserMinus} />
</span>
<span>Unsusbcribe</span>
@ -130,8 +131,8 @@ const ViewUser: FC = () => {
}
{subscriptionPending &&
<button className="button is-warning">
<span className="icon is-small">
<button style={{ backgroundColor: theme.blue, color: 'white' }}>
<span className="icon">
<FontAwesomeIcon icon={faUserClock} />
</span>
<span>Pending</span>
@ -139,8 +140,8 @@ const ViewUser: FC = () => {
}
{self && !isSelf && !subscribed && !subscriptionPending &&
<button className="button is-success" onClick={() => dispatch(subscribe(user.id))}>
<span className="icon is-small">
<button style={{ backgroundColor: theme.green, color: 'white' }} onClick={() => dispatch(subscribe(user.id))}>
<span className="icon">
<FontAwesomeIcon icon={faUserPlus} />
</span>
<span>Subscribe</span>
@ -148,8 +149,8 @@ const ViewUser: FC = () => {
}
{!isSelf &&
<button className="button is-danger">
<span className="icon is-small">
<button style={{ backgroundColor: theme.red, color: 'white' }}>
<span className="icon">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block</span>
@ -157,8 +158,8 @@ const ViewUser: FC = () => {
}
{user.group && !isGroup &&
<button className="button is-danger">
<span className="icon is-small">
<button style={{ backgroundColor: theme.red, color: 'white' }}>
<span className="icon">
<FontAwesomeIcon icon={faBan} />
</span>
<span>Block Community: {user.group.name}</span>

13
src/hooks/index.ts

@ -3,10 +3,10 @@ import { useSelector } from 'react-redux'
import { useHistory } from 'react-router-dom'
import isEqual from 'lodash/isEqual'
import { getAuthenticated, getChecked } from 'src/selectors/authentication'
import { getAuthenticated, getChecked, getAuthenticatedUser } from 'src/selectors/authentication'
import { getTheme } from 'src/selectors/theme'
import { getConfig } from 'src/selectors'
import { AppState, Theme, Config } from 'src/types'
import { AppState, Theme, Config, User } from 'src/types'
export const useAuthenticationCheck = () => {
const checked = useSelector<AppState, boolean>(getChecked)
@ -35,3 +35,12 @@ export const useDeepCompareEffect = (callback: EffectCallback, deps?: readonly a
}
export const useTheme = () => useSelector<AppState, Theme>(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)
if (!user || !user.settings) return defaultValue
return user.settings[key] ?? defaultValue
}

20
src/styles/app.css

@ -1,10 +1,10 @@
@charset "utf-8";
@import "normalize.css";
@import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro&display=swap');
@import url('https://fonts.googleapis.com/css?family=Quicksand:300,400,500,600,700&display=swap');
:root {
--default-border: 1px solid;
--default-font: 'Source Sans Pro', sans-serif;
--default-font: 'Quicksand', sans-serif;
--input-padding: 0.5rem 0.75rem;
--content-width: 600px;
--menu-width: 270px;
@ -12,7 +12,8 @@
html {
font-family: var(--default-font);
font-size: 18px;
font-size: 16px;
font-weight: 300;
}
body, div, h1, h2, input, select, textarea, label, button, p.help, section, div.icon {
@ -101,12 +102,18 @@ button, label.file-input {
border: none;
border-radius: 25px;
cursor: pointer;
font-family: var(--default-font);
font-size: 0.8rem;
font-weight: 700;
padding: 0.5rem 1rem;
min-width: 100px;
}
div.buttons {
display: flex;
justify-content: space-around;
}
div.logo-container {
padding-top: 1rem;
width: 60px;
@ -127,11 +134,14 @@ div.logo {
width: var(--size);
}
div.content-container {
width: var(--content-width);
}
div.content {
border-left: var(--default-border);
border-right: var(--default-border);
width: var(--content-width);
padding-bottom: 2rem;
padding-bottom: 3rem;
}
div.menu-container {

4
src/types/index.ts

@ -35,6 +35,10 @@ export interface LevelItem {
content: string
}
export interface Settings {
[key: string]: any
}
export interface Theme {
primary: string
primaryAlternate: string

1
src/types/store.ts

@ -38,6 +38,7 @@ export enum RequestKey {
Unsubscribe = 'unsubscribe',
UpdateApp = 'update-app',
UpdateGroup = 'update-group',
UpdateInstallationSettings = 'update-installation-settings',
UpdateSelf = 'update-self',
}

Loading…
Cancel
Save