Dwayne Harris 5 years ago
parent
commit
3527bd6e7e
  1. 4
      src/actions/posts.ts
  2. 36
      src/actions/users.ts
  3. 2
      src/components/app.tsx
  4. 3
      src/components/composer.tsx
  5. 6
      src/components/pages/create-app.tsx
  6. 33
      src/components/pages/edit-app.tsx
  7. 8
      src/components/pages/home.tsx
  8. 173
      src/components/pages/self.tsx
  9. 2
      src/components/pages/view-app.tsx
  10. 2
      src/components/pages/view-group.tsx
  11. 39
      src/components/timeline.tsx
  12. 2
      src/selectors/authentication.ts
  13. 9
      src/selectors/posts.ts
  14. 2
      src/types/entities.ts
  15. 10
      src/types/store.ts

4
src/actions/posts.ts

@ -69,7 +69,7 @@ export const fetchPost = (id: string): AppThunkAction => {
}
interface TimelineResponse {
posts: Entity[]
posts: Post[]
continuation?: string
}
@ -81,7 +81,7 @@ export const fetchTimeline = (continuation?: string): AppThunkAction => async di
path: `/api/timeline?${objectToQuerystring({ continuation })}`,
})
const posts = normalize(response.posts, EntityType.Group)
const posts = normalize(response.posts, EntityType.Post)
dispatch(setEntities(posts.entities))

36
src/actions/users.ts

@ -23,3 +23,39 @@ export const fetchUser = (id: string): AppThunkAction => {
}
}
}
export const subscribe = (id: string): AppThunkAction => {
return async dispatch => {
dispatch(startRequest(RequestKey.Subscribe))
try {
await apiFetch<Entity>({
path: `/api/user/${id}/subscribe`,
method: 'post',
})
dispatch(finishRequest(RequestKey.Subscribe, true))
} catch (err) {
dispatch(finishRequest(RequestKey.Subscribe, false))
throw err
}
}
}
export const unsubscribe = (id: string): AppThunkAction => {
return async dispatch => {
dispatch(startRequest(RequestKey.Unsubscribe))
try {
await apiFetch<Entity>({
path: `/api/user/${id}/unsubscribe`,
method: 'post',
})
dispatch(finishRequest(RequestKey.Unsubscribe, true))
} catch (err) {
dispatch(finishRequest(RequestKey.Unsubscribe, false))
throw err
}
}
}

2
src/components/app.tsx

@ -110,7 +110,7 @@ const App: FC = () => {
<Route path="/communities">
<Groups />
</Route>
<Route path="/self/:tab?">
<Route path="/self">
<Self />
</Route>
<Route path="/apps">

3
src/components/composer.tsx

@ -6,7 +6,7 @@ import { getOrigin } from 'src/utils'
import { useConfig, useDeepCompareEffect } from 'src/hooks'
import { fetchInstallations, setSelectedInstallation, setHeight as setComposerHeight, setError as setComposerError } from 'src/actions/composer'
import { showNotification } from 'src/actions/notifications'
import { createPost } from 'src/actions/posts'
import { createPost, fetchTimeline } from 'src/actions/posts'
import { getInstallations, getSelectedInstallation, getError, getHeight as getComposerHeight } from 'src/selectors/composer'
import { AppState, Installation, ClassDictionary, AppThunkDispatch, NotificationType } from 'src/types'
import { IncomingMessageData, OutgoingMessageData } from 'src/types/communicator'
@ -127,6 +127,7 @@ const Composer: FC = () => {
})
dispatch(showNotification(NotificationType.Success, `Posted!`))
dispatch(fetchTimeline())
} catch (err) {
postMessage({
name,

6
src/components/pages/create-app.tsx

@ -40,6 +40,9 @@ const CreateApp: FC = () => {
const version = valueFromForm<string>(form, 'version')
const composerUrl = valueFromForm<string>(form, 'composerUrl')
const rendererUrl = valueFromForm<string>(form, 'rendererUrl')
const imageUrl = valueFromForm<string>(form, 'image')
const coverImageUrl = valueFromForm<string>(form, 'coverImage')
const iconImageUrl = valueFromForm<string>(form, 'iconImage')
const agree = valueFromForm<boolean>(form, 'agree')
if (!name || name === '') {
@ -68,6 +71,9 @@ const CreateApp: FC = () => {
companyName,
composerUrl,
rendererUrl,
imageUrl,
coverImageUrl,
iconImageUrl,
}))
history.push(`/a/${id}`)

33
src/components/pages/edit-app.tsx

@ -1,8 +1,8 @@
import React, { FC, useEffect } from 'react'
import React, { FC, useEffect, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faIdCard, faCheckCircle } from '@fortawesome/free-solid-svg-icons'
import { faIdCard, faCheckCircle, faKey, faShieldAlt } from '@fortawesome/free-solid-svg-icons'
import { handleApiError } from 'src/api/errors'
import { fetchApp, updateApp } from 'src/actions/apps'
@ -38,6 +38,7 @@ const EditApp: FC = () => {
const selfId = useSelector<AppState, string | undefined>(getAuthenticatedUserId)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
const [showPrivateKey, setShowPrivateKey] = useState(false)
useEffect(() => {
try {
@ -110,17 +111,45 @@ const EditApp: FC = () => {
}))
dispatch(showNotification(NotificationType.Success, 'Updated'))
history.push(`/a/${app.id}`)
} catch (err) {
handleApiError(err, dispatch, history)
}
}
const privateKeyDisplay = showPrivateKey ? app.privateKey : ''
return (
<div>
<PageHeader title={app.name} />
<div className="main-content">
<div className="centered-content">
<div className="field">
<label className="label">Public Key</label>
<div className="control has-icons-left">
<input className="input" type="text" value={app.publicKey} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faKey} />
</span>
</div>
</div>
<br />
<div className="field has-addons">
<p className="control">
<button className="button" onClick={() => setShowPrivateKey(!showPrivateKey)}>
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faShieldAlt} />
</span>
</button>
</p>
<p className="control is-expanded">
<input className="input" type="text" value={privateKeyDisplay} placeholder="Private Key" readOnly />
</p>
</div>
<br /><hr />
<div className="field">
<label className="label">ID</label>
<div className="control has-icons-left">

8
src/components/pages/home.tsx

@ -7,6 +7,7 @@ import { AppState } from 'src/types'
import PageHeader from 'src/components/page-header'
import Composer from 'src/components/composer'
import Timeline from 'src/components/timeline'
const Home: FC = () => {
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
@ -20,7 +21,12 @@ const Home: FC = () => {
<PageHeader title="Home" />
<div className="main-content">
{authenticated && <Composer />}
{authenticated &&
<div>
<Composer />
<Timeline />
</div>
}
</div>
</div>
)

173
src/components/pages/self.tsx

@ -1,9 +1,8 @@
import React, { FC } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Link, useParams, useHistory } from 'react-router-dom'
import moment from 'moment'
import { Link, useHistory } from 'react-router-dom'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faDoorOpen, faCheckCircle, faPlusCircle, faIdCard, faEnvelope, faUserShield } from '@fortawesome/free-solid-svg-icons'
import { faDoorOpen, faCheckCircle, faIdCard, faEnvelope, faUserShield } from '@fortawesome/free-solid-svg-icons'
import { unauthenticate, updateSelf } from 'src/actions/authentication'
import { initForm, initField } from 'src/actions/forms'
@ -26,18 +25,7 @@ import CheckboxField from 'src/components/forms/checkbox-field'
import ImageField from 'src/components/forms/image-field'
import CoverImageField from 'src/components/forms/cover-image-field'
interface Params {
tab: string
}
const tabs: Tab[] = [
{ id: '', label: 'Posts' },
{ id: 'settings', label: 'Settings' },
{ id: 'apps', label: 'Apps' },
]
const Self: FC = () => {
const { tab = '' } = useParams<Params>()
const dispatch = useDispatch()
const history = useHistory()
@ -98,106 +86,69 @@ const Self: FC = () => {
<UserInfo user={user} />
<div className="centered-content">
<div className="tabs is-large">
<ul>
{tabs.map(t => (
<li key={t.id} className={tab === t.id ? 'is-active': ''}>
<Link to={`/self/${t.id}`}>
{t.label}
</Link>
</li>
))}
</ul>
<Link to={`/u/${user.id}`}>View Your Page</Link>
<br /><br />
<div className="field">
<label className="label">ID</label>
<div className="control has-icons-left">
<input className="input" type="text" value={user.id} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faIdCard} />
</span>
</div>
</div>
<div className="container">
{tab === '' &&
<p>No Posts.</p>
}
{tab === 'settings' &&
<div>
<Link to={`/u/${user.id}`}>View Your Page</Link>
<br /><br />
<div className="field">
<label className="label">ID</label>
<div className="control has-icons-left">
<input className="input" type="text" value={user.id} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faIdCard} />
</span>
</div>
</div>
<br />
<div className="field">
<label className="label">Email</label>
<div className="control has-icons-left">
<input className="input" type="email" value={user.email} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faEnvelope} />
</span>
</div>
</div>
<br />
<TextField name="name" label="Name" placeholder="Your Display Name" />
<br />
<TextareaField name="about" label="About" placeholder="Your Bio" />
<br />
<SelectField name="privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<br />
<ImageField name="image" label="Avatar" />
<br />
<CoverImageField name="coverImage" />
<br />
<CheckboxField name="requiresApproval">
You must approve each Subscription request from other users.
</CheckboxField>
<br /><br />
<nav className="level">
<div className="level-left">
<p className="level-item">
<button className="button is-primary" onClick={() => handleUpdate()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faCheckCircle} />
</span>
<span>Save</span>
</button>
</p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-danger" onClick={() => handleLogout()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faDoorOpen} />
</span>
<span>Log Out</span>
</button>
</p>
</div>
</nav>
</div>
}
{tab === 'apps' &&
<div>
<p>No Apps.</p>
<br />
<Link to="/developers/create" className="button is-primary">
<br />
<div className="field">
<label className="label">Email</label>
<div className="control has-icons-left">
<input className="input" type="email" value={user.email} readOnly />
<span className="icon is-small is-left">
<FontAwesomeIcon icon={faEnvelope} />
</span>
</div>
</div>
<br />
<TextField name="name" label="Name" placeholder="Your Display Name" />
<br />
<TextareaField name="about" label="About" placeholder="Your Bio" />
<br />
<SelectField name="privacy" label="Privacy" options={PRIVACY_OPTIONS} icon={faUserShield} />
<br />
<ImageField name="image" label="Avatar" />
<br />
<CoverImageField name="coverImage" />
<br />
<CheckboxField name="requiresApproval">
You must approve each Subscription request from other users.
</CheckboxField>
<br /><br />
<nav className="level">
<div className="level-left">
<p className="level-item">
<button className="button is-primary" onClick={() => handleUpdate()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faPlusCircle} />
<FontAwesomeIcon icon={faCheckCircle} />
</span>
<span>Create a new App</span>
</Link>
</div>
}
</div>
<span>Save</span>
</button>
</p>
</div>
<div className="level-right">
<p className="level-item">
<button className="button is-danger" onClick={() => handleLogout()}>
<span className="icon is-small">
<FontAwesomeIcon icon={faDoorOpen} />
</span>
<span>Log Out</span>
</button>
</p>
</div>
</nav>
</div>
</div>
</div>

2
src/components/pages/view-app.tsx

@ -126,7 +126,7 @@ const ViewApp: FC = () => {
<div className="buttons">
{renderButton()}
{isCreator && <Link className="button is-warning" to={`/a/${id}/edit`}>Edit App</Link>}
{isCreator && <Link className="button is-primary" to={`/a/${id}/edit`}>View/Edit App</Link>}
</div>
</div>
</div>

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

@ -94,7 +94,7 @@ const ViewGroup: FC = () => {
}
{isAdmin &&
<Link to={`/c/${group.id}/admin/`} className="button is-warning">
<Link to={`/c/${group.id}/admin/`} className="button is-primary">
<span className="icon is-small">
<FontAwesomeIcon icon={faEdit} />
</span>

39
src/components/timeline.tsx

@ -0,0 +1,39 @@
import React, { FC, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useHistory } from 'react-router-dom'
import { handleApiError } from 'src/api/errors'
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 PostList from 'src/components/post-list'
const Timeline: FC = () => {
const authenticated = useSelector<AppState, boolean>(getAuthenticated)
const posts = useSelector<AppState, Post[]>(getTimeline)
const dispatch = useDispatch<AppThunkDispatch>()
const history = useHistory()
useEffect(() => {
const init = async () => {
try {
await dispatch(fetchTimeline())
} catch (err) {
handleApiError(err, dispatch, history)
}
}
if (authenticated) init()
}, [authenticated])
return (
<div>
<PostList posts={posts} />
</div>
)
}
export default Timeline

2
src/selectors/authentication.ts

@ -5,7 +5,7 @@ import { denormalize } from 'src/utils/normalization'
import { AppState, User, EntityType } from 'src/types'
export const getChecked = (state: AppState) => state.authentication.checked
export const getAuthenticated = (state: AppState) => state.authentication.authenticated
export const getAuthenticated = (state: AppState) => state.authentication.checked && state.authentication.authenticated
export const getAuthenticatedUserId = (state: AppState) => state.authentication.userId
export const getAuthenticatedUser = createSelector(

9
src/selectors/posts.ts

@ -1,5 +1,12 @@
import { denormalize } from 'src/utils/normalization'
import { AppState, Post, EntityType } from 'src/types'
import { AppState, Post, EntityType, EntityListKey } from 'src/types'
export const getTimeline = (state: AppState) => {
const entityList = state.lists[EntityListKey.Timeline]
if (!entityList) return []
return denormalize(entityList.entities, EntityType.Post, state.entities) as Post[]
}
export const getUserPosts = (state: AppState, id: string) => {
const entityList = state.lists[`posts:${id}`]

2
src/types/entities.ts

@ -50,6 +50,8 @@ type BaseUser = Entity & {
coverImageUrl?: string
requiresApproval: boolean
privacy: string
subscribed: boolean
subscribedToYou: boolean
}
export type User = BaseUser & {

10
src/types/store.ts

@ -15,8 +15,8 @@ export enum RequestKey {
CreateGroup = 'create-group',
CreateInvitation = 'create-invitation',
CreatePost = 'create-post',
FetchAppAvailability = 'fetch-app-availability',
FetchApp = 'fetch-app',
FetchAppAvailability = 'fetch-app-availability',
FetchApps = 'fetch-apps',
FetchGroup = 'fetch-group',
FetchGroupAvailability = 'fetch-group-availability',
@ -26,14 +26,16 @@ export enum RequestKey {
FetchInstallations = 'fetch-installations',
FetchInvitations = 'fetch-invitations',
FetchPost = 'fetch-post',
FetchTimeline = 'fetch-timeline',
FetchUserPosts = 'fetch-user-posts',
FetchSelfApps = 'fetch-self-apps',
FetchTimeline = 'fetch-timeline',
FetchUser = 'fetch-user',
FetchUserAvailability = 'fetch-user-availability',
FetchUserPosts = 'fetch-user-posts',
InstallApp = 'install-app',
UninstallApp = 'uninstall-app',
Register = 'register',
Subscribe = 'subscribe',
UninstallApp = 'uninstall-app',
Unsubscribe = 'unsubscribe',
UpdateGroup = 'update-group',
UpdateSelf = 'update-self',
}

Loading…
Cancel
Save