Showing
58 changed files
with
4270 additions
and
0 deletions
.gitignore
0 → 100644
React/inbox/api-routes/index.ts
0 → 100644
1 | +import composeApiRouteFor from '../../../helpers/composeApiRouteFor'; | |
2 | + | |
3 | +const communicationsBase = composeApiRouteFor('communication-threads'); | |
4 | + | |
5 | +export const threadContentsRoute = (threadId: number) => `${communicationsBase}/${threadId}/contents`; | |
6 | + | |
7 | +// TODO: should be replaced by threadContentsRoute once API has been adjusted | |
8 | +export const threadMessagesRoute = (threadId: number) => `${communicationsBase}/${threadId}/messages`; | ... | ... |
React/inbox/components/ChatInput/index.tsx
0 → 100644
1 | +import React, {useState} from 'react'; | |
2 | +import {useDebounce} from 'use-debounce'; | |
3 | +import {size, trim} from 'lodash/fp'; | |
4 | +import {useDispatch, useSelector} from 'react-redux'; | |
5 | +import { postThreadContent } from '../../middlewares/thread-contents'; | |
6 | +import {getCurrentThreadId} from '../../../selectors/thread'; | |
7 | + | |
8 | +const ChatInput = () => { | |
9 | + const dispatch = useDispatch(); | |
10 | + const [inputValue, setInputValue] = useState(''); | |
11 | + const [message] = useDebounce(trim(inputValue), 300); | |
12 | + const isBlankMessage = size(message) === 0; | |
13 | + const submitDisabled = isBlankMessage; | |
14 | + const threadId = useSelector(getCurrentThreadId); | |
15 | + const handleSubmit = () => { | |
16 | + dispatch(postThreadContent({threadId, message: inputValue})); | |
17 | + }; | |
18 | + const handleInputChange = (value: string) => { | |
19 | + setInputValue(value); | |
20 | + return value; | |
21 | + }; | |
22 | + | |
23 | + return ( | |
24 | + <div className='chat-input-container'> | |
25 | + <form onSubmit={handleSubmit}> | |
26 | + <div className='collapse row align-middle mb-1'> | |
27 | + <div className='auto columns'> | |
28 | + <textarea placeholder={`What's on your mind?`} onChange={({target}) => handleInputChange(target.value)} /> | |
29 | + </div> | |
30 | + {/* DZ */} | |
31 | + <div className='shrink columns'> | |
32 | + {/* TODO: Insert <Dropzone>*/} | |
33 | + <button data-tip data-for='attachment-criteria' className='button secondary hollow large clear pull-left'> | |
34 | + <i className='fa fa-paperclip'/> | |
35 | + </button> | |
36 | + {/* TODO: Insert <ReactTooltip>*/} | |
37 | + <button disabled={submitDisabled} type='button' onClick={handleSubmit} className='button success hollow large clear pull-right'> | |
38 | + <i className='fa fa-paper-plane'/> | |
39 | + </button> | |
40 | + </div> | |
41 | + </div> | |
42 | + </form> | |
43 | + </div> | |
44 | + ); | |
45 | +}; | |
46 | + | |
47 | +export default ChatInput; | ... | ... |
1 | +import React from 'react'; | |
2 | +import {map, getOr} from 'lodash/fp'; | |
3 | +import {useSelector} from 'react-redux'; | |
4 | +import {plainToClass} from 'class-transformer'; | |
5 | +import ThreadContent from '../../../../../models/ThreadContent'; | |
6 | +import ThreadAttachment, {getAttachmentFileTypeCssClass} from '../../../../../models/ThreadAttachment'; | |
7 | +import {downloadAttachmentRoute} from '../../../../../api-routes'; | |
8 | +import {getCurrentThreadId} from '../../../../selectors/thread'; | |
9 | + | |
10 | +interface Props { | |
11 | + attachments: ThreadContent[]; | |
12 | +} | |
13 | + | |
14 | +const FileAttachments = ({attachments}: Props) => { | |
15 | + if (!attachments.length) { | |
16 | + return null; | |
17 | + } | |
18 | + const threadId = useSelector(getCurrentThreadId); | |
19 | + return ( | |
20 | + <ul className='attachments inline-list'> | |
21 | + {map(({id, contentInstance}) => { | |
22 | + const obj = plainToClass(ThreadAttachment, contentInstance); | |
23 | + const caption = getOr('', 'media.caption', obj); | |
24 | + return ( | |
25 | + <li key={`file_attachment_${id}`} className={getAttachmentFileTypeCssClass(obj)}> | |
26 | + <a href={downloadAttachmentRoute(threadId, obj.id)} target='_blank' className='btn-dl' > | |
27 | + <i className='fa fa-download'/> | |
28 | + </a> | |
29 | + <span className='filename' data-tooltip title={caption}> | |
30 | + {caption} | |
31 | + </span> | |
32 | + </li>); | |
33 | + }, | |
34 | + attachments)} | |
35 | + </ul> | |
36 | + ); | |
37 | +}; | |
38 | +export default FileAttachments; | ... | ... |
1 | +import React from 'react'; | |
2 | +import { getOr } from 'lodash/fp'; | |
3 | +import ThreadContent from '../../../../../models/ThreadContent'; | |
4 | +import isCurrentUser from '../../../../helpers/isCurrentUser'; | |
5 | +interface Props { | |
6 | + threadContent: ThreadContent; | |
7 | +} | |
8 | + | |
9 | +const Info = ({threadContent}: Props) => { | |
10 | + const readBy = getOr(0, 'contentInstance.readBy', threadContent); | |
11 | + const authorId = getOr(0, 'author.id', threadContent); | |
12 | + const isSeen = !isCurrentUser(authorId) && readBy; | |
13 | + return ( | |
14 | + <span className='subheader timestamp'> | |
15 | + {isSeen && <small className='pull-left mr-1'> | |
16 | + <i className='fa fa-check-circle'/> Seen | |
17 | + </small>} | |
18 | + <small>{threadContent.getLocalTimeString()}</small> | |
19 | + </span> | |
20 | + ); | |
21 | +}; | |
22 | + | |
23 | +export default Info; | ... | ... |
1 | +import React from 'react'; | |
2 | +import {get} from 'lodash/fp'; | |
3 | +import ThreadContent from '../../../../../models/ThreadContent'; | |
4 | +import Text from '../Text'; | |
5 | +import Info from '../Info'; | |
6 | + | |
7 | +interface Props { | |
8 | + threadContent: ThreadContent; | |
9 | +} | |
10 | + | |
11 | +export default ({threadContent}: Props) => { | |
12 | + return ( | |
13 | + <div className='text-align-right' data-msg-key={get('contentInstance.id', threadContent)}> | |
14 | + <div className='wrapper inline-block'> | |
15 | + <div className='media-object pull-right' data-animate='slide-in-right slide-out-right'> | |
16 | + <div className='small white-bg talk-bubble border tri-right right-in'> | |
17 | + <Text threadContent={threadContent} /> | |
18 | + </div> | |
19 | + <Info threadContent={threadContent} /> | |
20 | + </div> | |
21 | + </div> | |
22 | + </div> | |
23 | + ); | |
24 | +}; | ... | ... |
1 | +import React from 'react'; | |
2 | +import {getOr} from 'lodash/fp'; | |
3 | +import FileAttachments from '../FileAttachments'; | |
4 | +import ThreadContent from '../../../../../models/ThreadContent'; | |
5 | +import {fileAttachments} from '../../../../../helpers/threadContent'; | |
6 | + | |
7 | +interface Props { | |
8 | + threadContent: ThreadContent; | |
9 | +} | |
10 | + | |
11 | +const Text = ({threadContent}: Props) => { | |
12 | + | |
13 | + return ( | |
14 | + <div className='talktext'> | |
15 | + <p dangerouslySetInnerHTML={{__html: getOr('', 'contentInstance.contentText', threadContent)}}/> | |
16 | + <FileAttachments attachments={fileAttachments(threadContent)} /> | |
17 | + </div> | |
18 | + ); | |
19 | +}; | |
20 | + | |
21 | +export default Text; | ... | ... |
1 | +import React from 'react'; | |
2 | +import {get} from 'lodash/fp'; | |
3 | +import ThreadContent from '../../../../../models/ThreadContent'; | |
4 | +import Text from '../Text'; | |
5 | +import Info from '../Info'; | |
6 | +import UserImageBubble from '../../../../../app/components/UserImageBubble'; | |
7 | +import {displayName, profilePicture} from '../../../../../helpers/user'; | |
8 | + | |
9 | +interface Props { | |
10 | + threadContent: ThreadContent; | |
11 | +} | |
12 | +const TheirMessage = ({threadContent}: Props) => { | |
13 | + | |
14 | + const {author} = threadContent; | |
15 | + const imageProps = { | |
16 | + displayName: displayName(author), | |
17 | + profilePictureUrl: profilePicture(author) | |
18 | + }; | |
19 | + | |
20 | + return ( | |
21 | + <div className='wrapper' data-msg-key={get('contentInstance.id', threadContent)}> | |
22 | + <div className='media-object' data-animate='slide-in-right slide-out-right'> | |
23 | + <UserImageBubble {...imageProps} /> | |
24 | + <div className='media-object-section middle'> | |
25 | + <div className='small talk-bubble border tri-right left-in'> | |
26 | + <Text threadContent={threadContent} /> | |
27 | + </div> | |
28 | + <Info threadContent={threadContent} /> | |
29 | + </div> | |
30 | + </div> | |
31 | + </div> | |
32 | + ); | |
33 | +}; | |
34 | +export default TheirMessage; | ... | ... |
React/inbox/components/ChatMessage/index.tsx
0 → 100644
1 | +import React from 'react'; | |
2 | +import {getOr} from 'lodash/fp'; | |
3 | +import ThreadContent from '../../../../models/ThreadContent'; | |
4 | +import TheirMessage from './TheirMessage'; | |
5 | +import isCurrentUser from '../../../helpers/isCurrentUser'; | |
6 | +import MyMessage from './MyMessage'; | |
7 | + | |
8 | +interface Props { | |
9 | + threadContent: ThreadContent; | |
10 | +} | |
11 | + | |
12 | +const ChatMessage = ({threadContent}: Props) => { | |
13 | + return isCurrentUser(getOr(0, 'author.id', threadContent)) | |
14 | + ? <MyMessage threadContent={threadContent} /> | |
15 | + : <TheirMessage threadContent={threadContent} />; | |
16 | +}; | |
17 | + | |
18 | +export default ChatMessage; | ... | ... |
React/inbox/components/Thread/index.tsx
0 → 100644
1 | +/** | |
2 | + * Inbox tab main chat container component | |
3 | + */ | |
4 | + | |
5 | +import React, { useEffect } from 'react'; | |
6 | +import { useDispatch, useSelector } from 'react-redux'; | |
7 | +import { map } from 'lodash/fp'; | |
8 | +import {didSetActiveTab} from '../../../tabs/actions'; | |
9 | +import {COMMUNICATIONS} from '../../../tabs/constants'; | |
10 | +import {LoadingRipple} from '../../../../utilities/components/LoadingRipple'; | |
11 | +import PageLayout from '../../../components/PageLayout'; | |
12 | +import {getCurrentThreadId} from '../../../selectors/thread'; | |
13 | +import {fetchThreadContents} from '../../middlewares/thread-contents'; | |
14 | +import {getIsFetchingThreadContents, getThreadContentsCollection} from '../../selectors'; | |
15 | +import ChatMessage from '../ChatMessage'; | |
16 | +import ThreadContent, {ThreadContentTypes} from '../../../../models/ThreadContent'; | |
17 | +import ChatInput from '../ChatInput'; | |
18 | + | |
19 | +const Thread = () => { | |
20 | + const dispatch = useDispatch(); | |
21 | + useEffect(() => { | |
22 | + dispatch(didSetActiveTab(COMMUNICATIONS)); | |
23 | + }, []); | |
24 | + | |
25 | + const threadId = useSelector(getCurrentThreadId); | |
26 | + const isFetching = useSelector(getIsFetchingThreadContents); | |
27 | + const collection = useSelector(getThreadContentsCollection); | |
28 | + | |
29 | + useEffect(() => { | |
30 | + dispatch(fetchThreadContents(threadId)); | |
31 | + }, [threadId]); | |
32 | + | |
33 | + return ( | |
34 | + <PageLayout> | |
35 | + <div id='chat-container' style={{overflowY: 'hidden'}}> | |
36 | + <div className={`chat-body-container`}> | |
37 | + {isFetching && <LoadingRipple isLoading />} | |
38 | + {!isFetching && | |
39 | + <div className='chat-body'> | |
40 | + {map( | |
41 | + (each: ThreadContent) => ( | |
42 | + ThreadContentTypes.MESSAGE === each.type ? <ChatMessage | |
43 | + threadContent={each} | |
44 | + key={`thread_content_${each.id}`} | |
45 | + /> : null | |
46 | + ), collection | |
47 | + )} | |
48 | + </div> | |
49 | + } | |
50 | + </div> | |
51 | + <ChatInput /> | |
52 | + </div> | |
53 | + </PageLayout> | |
54 | + ); | |
55 | +}; | |
56 | + | |
57 | +export default Thread; | ... | ... |
1 | +import { getThunkActionCreator } from 'redux-thunk-routine'; | |
2 | +import { plainToClass } from 'class-transformer'; | |
3 | +import { fetchThreadContents as fetchThreadContentsRoutine, postThreadContent as postThreadContentRoutine } from './routines'; | |
4 | +import AlertifyHelper from '../../../../helpers/AlertifyHelper'; | |
5 | +import {threadContentsRoute, threadMessagesRoute} from '../../api-routes'; | |
6 | +import {apiGet, apiPost} from '../../../../helpers/api'; | |
7 | +import ThreadContent from '../../../../models/ThreadContent'; | |
8 | + | |
9 | +export const fetchThreadContents = getThunkActionCreator( | |
10 | + fetchThreadContentsRoutine, | |
11 | + async (threadId: number, page: number = 1) => { | |
12 | + try { | |
13 | + const { threadContents, meta } = await apiGet( | |
14 | + threadContentsRoute(threadId), | |
15 | + { page } | |
16 | + ); | |
17 | + | |
18 | + return {collection: plainToClass(ThreadContent, threadContents), meta}; | |
19 | + } catch (e) { | |
20 | + AlertifyHelper.error('Unable to fetch thread contents'); | |
21 | + throw e; | |
22 | + } | |
23 | + } | |
24 | +); | |
25 | + | |
26 | +export const postThreadContent = getThunkActionCreator( | |
27 | + postThreadContentRoutine, | |
28 | + async ({threadId, message}: {threadId: number, message: string}) => { | |
29 | + try { | |
30 | + // todo: append attachment ids | |
31 | + const attachmentIds: number[] = []; | |
32 | + const { threadContent } = await apiPost(threadMessagesRoute(threadId), { | |
33 | + message, | |
34 | + threadAttachments: attachmentIds | |
35 | + }); | |
36 | + return plainToClass(ThreadContent, threadContent); | |
37 | + } catch (e) { | |
38 | + AlertifyHelper.error('Failed to send your message.'); | |
39 | + throw e; | |
40 | + } | |
41 | + } | |
42 | +); | ... | ... |
React/inbox/page-paths.ts
0 → 100644
React/inbox/reducers.ts
0 → 100644
1 | +import { sortBy, unionBy, get, set } from 'lodash/fp'; | |
2 | +import { createReducer } from '../../helpers/Reducer'; | |
3 | +import {fetchThreadContents as fetchThreadContentsRoutine, postThreadContent} from './middlewares/thread-contents/routines'; | |
4 | +import {ThreadContentsState} from '../../definitions/portal-interfaces'; | |
5 | +import ThreadContent from '../../models/ThreadContent'; | |
6 | + | |
7 | +const initialState = { | |
8 | + collection: [], | |
9 | + meta: null, | |
10 | + isFetching: false, | |
11 | + isFetched: false | |
12 | +}; | |
13 | + | |
14 | +const fetchThreadContentsHandler = () => ({ | |
15 | + [fetchThreadContentsRoutine.REQUEST]: (state: ThreadContentsState) => ({ | |
16 | + ...state, | |
17 | + isFetching: true, | |
18 | + isFetched: false | |
19 | + }), | |
20 | + [fetchThreadContentsRoutine.SUCCESS]: ( | |
21 | + state: ThreadContentsState, | |
22 | + {payload: {collection, meta}}: {payload: ThreadContentsState}) => { | |
23 | + const newCollection = unionBy((each: ThreadContent) => get('id', each), collection, state.collection); | |
24 | + const orderedCollection = sortBy('createdAt', newCollection); | |
25 | + return { | |
26 | + ...state, | |
27 | + collection: orderedCollection, | |
28 | + meta, | |
29 | + isFetching: false, | |
30 | + isFetched: true | |
31 | + }; | |
32 | + }, | |
33 | + [fetchThreadContentsRoutine.FAILURE]: (state: ThreadContentsState) => ({ | |
34 | + ...state, | |
35 | + isFetching: false, | |
36 | + isFetched: true | |
37 | + }) | |
38 | +}); | |
39 | + | |
40 | +const postThreadContentHandler = () => ({ | |
41 | + [postThreadContent.SUCCESS]: ( | |
42 | + state: ThreadContentsState, | |
43 | + { payload: threadContent }: { payload: ThreadContent }) => { | |
44 | + return set('collection', [...state.collection, threadContent], state); | |
45 | + } | |
46 | +}); | |
47 | + | |
48 | +export default createReducer(initialState, { | |
49 | + ...fetchThreadContentsHandler(), | |
50 | + ...postThreadContentHandler() | |
51 | +}); | ... | ... |
React/inbox/selectors.ts
0 → 100644
1 | +import { createSelector } from 'reselect'; | |
2 | +import { get } from 'lodash/fp'; | |
3 | +import { PortalState } from '../../definitions/portal-interfaces'; | |
4 | + | |
5 | +const threadContents = ({threadContents}: PortalState) => threadContents; | |
6 | + | |
7 | +export const getThreadContentsCollection = createSelector( | |
8 | + threadContents, | |
9 | + get('collection') | |
10 | +); | |
11 | + | |
12 | +export const getIsFetchingThreadContents = createSelector( | |
13 | + threadContents, | |
14 | + get('isFetching') | |
15 | +); | ... | ... |
1 | +import React from 'react'; | |
2 | +import { StyleSheet } from 'react-native'; | |
3 | +import ListGrid, { | |
4 | + HeaderRow, | |
5 | + ItemRow, | |
6 | + PlayerColumn, | |
7 | + PPGColumn | |
8 | +} from '../../ListGrid'; | |
9 | +import HeaderColumn from '../../ListGrid/HeaderColumn'; | |
10 | +import { BorderRadius, CardHeight } from '../constants'; | |
11 | + | |
12 | +const styles = StyleSheet.create({ | |
13 | + itemWrapper: { | |
14 | + height: CardHeight, | |
15 | + borderRadius: BorderRadius | |
16 | + }, | |
17 | + player: { | |
18 | + justifyContent: 'center' | |
19 | + }, | |
20 | + stats: { | |
21 | + justifyContent: 'center', | |
22 | + alignItems: 'center' | |
23 | + } | |
24 | +}); | |
25 | + | |
26 | +const AveragesList: React.FC<any> = () => { | |
27 | + return ( | |
28 | + <ListGrid style={{ marginTop: 10 }}> | |
29 | + <HeaderRow style={{ marginBottom: 5 }}> | |
30 | + <HeaderColumn align="center" size={3} fontSize="sm" label="PLAYER" /> | |
31 | + <HeaderColumn align="center" size={1.5} fontSize="sm" label="1-week" /> | |
32 | + <HeaderColumn align="center" size={1.5} fontSize="sm" label="2-week" /> | |
33 | + <HeaderColumn align="center" size={1.5} fontSize="sm" label="4-week" /> | |
34 | + <HeaderColumn align="center" size={1.5} fontSize="sm" label="Season" /> | |
35 | + </HeaderRow> | |
36 | + <ItemRow style={styles.itemWrapper}> | |
37 | + <PlayerColumn | |
38 | + name="EZEKEL ELLIOT" | |
39 | + position="RB" | |
40 | + team="DAL" | |
41 | + rating={4.3} | |
42 | + size={3} | |
43 | + style={styles.player} | |
44 | + /> | |
45 | + <PPGColumn size={1.7} style={styles.stats}> | |
46 | + 24.5 | |
47 | + </PPGColumn> | |
48 | + <PPGColumn size={1.5} style={styles.stats}> | |
49 | + 14.9 | |
50 | + </PPGColumn> | |
51 | + <PPGColumn size={1.5} style={styles.stats}> | |
52 | + 15.8 | |
53 | + </PPGColumn> | |
54 | + <PPGColumn size={1.5} style={styles.stats}> | |
55 | + 24.5 | |
56 | + </PPGColumn> | |
57 | + </ItemRow> | |
58 | + <ItemRow style={styles.itemWrapper}> | |
59 | + <PlayerColumn | |
60 | + name="MICHAEL THOMAS" | |
61 | + position="WR" | |
62 | + team="NO" | |
63 | + rating={3.5} | |
64 | + size={3} | |
65 | + style={styles.player} | |
66 | + /> | |
67 | + <PPGColumn size={1.5} style={styles.stats}> | |
68 | + 33.2 | |
69 | + </PPGColumn> | |
70 | + <PPGColumn size={1.5} style={styles.stats}> | |
71 | + 31.6 | |
72 | + </PPGColumn> | |
73 | + <PPGColumn size={1.5} style={styles.stats}> | |
74 | + 16.9 | |
75 | + </PPGColumn> | |
76 | + <PPGColumn size={1.5} style={styles.stats}> | |
77 | + 33.2 | |
78 | + </PPGColumn> | |
79 | + </ItemRow> | |
80 | + <ItemRow style={styles.itemWrapper}> | |
81 | + <PlayerColumn | |
82 | + name="JULIO JONES" | |
83 | + position="WR" | |
84 | + team="ATL" | |
85 | + rating={4} | |
86 | + size={3} | |
87 | + style={styles.player} | |
88 | + /> | |
89 | + <PPGColumn size={1.5} style={styles.stats}> | |
90 | + 33.2 | |
91 | + </PPGColumn> | |
92 | + <PPGColumn size={1.5} style={styles.stats}> | |
93 | + 17.7 | |
94 | + </PPGColumn> | |
95 | + <PPGColumn size={1.5} style={styles.stats}> | |
96 | + 25.9 | |
97 | + </PPGColumn> | |
98 | + <PPGColumn size={1.5} style={styles.stats}> | |
99 | + 33.2 | |
100 | + </PPGColumn> | |
101 | + </ItemRow> | |
102 | + <ItemRow style={styles.itemWrapper}> | |
103 | + <PlayerColumn | |
104 | + name="DESEAN WATSON" | |
105 | + position="QB" | |
106 | + team="HOU" | |
107 | + rating={4} | |
108 | + size={3} | |
109 | + style={styles.player} | |
110 | + /> | |
111 | + <PPGColumn size={1.5} style={styles.stats}> | |
112 | + 24.5 | |
113 | + </PPGColumn> | |
114 | + <PPGColumn size={1.5} style={styles.stats}> | |
115 | + 19.9 | |
116 | + </PPGColumn> | |
117 | + <PPGColumn size={1.5} style={styles.stats}> | |
118 | + 15.6 | |
119 | + </PPGColumn> | |
120 | + <PPGColumn size={1.5} style={styles.stats}> | |
121 | + 24.5 | |
122 | + </PPGColumn> | |
123 | + </ItemRow> | |
124 | + <ItemRow style={styles.itemWrapper}> | |
125 | + <PlayerColumn | |
126 | + owned | |
127 | + name="COOPER KUPP" | |
128 | + position="WR" | |
129 | + team="STL" | |
130 | + rating={3} | |
131 | + size={3} | |
132 | + style={styles.player} | |
133 | + /> | |
134 | + <PPGColumn size={1.5} style={styles.stats}> | |
135 | + 33.2 | |
136 | + </PPGColumn> | |
137 | + <PPGColumn size={1.5} style={styles.stats}> | |
138 | + 21.8 | |
139 | + </PPGColumn> | |
140 | + <PPGColumn size={1.5} style={styles.stats}> | |
141 | + 15.8 | |
142 | + </PPGColumn> | |
143 | + <PPGColumn size={1.5} style={styles.stats}> | |
144 | + 33.2 | |
145 | + </PPGColumn> | |
146 | + </ItemRow> | |
147 | + </ListGrid> | |
148 | + ); | |
149 | +}; | |
150 | + | |
151 | +export default AveragesList; | ... | ... |
1 | +import { get } from 'lodash/fp'; | |
2 | +import React from 'react'; | |
3 | +import { StyleSheet } from 'react-native'; | |
4 | +import { TouchableOpacity } from 'react-native-gesture-handler'; | |
5 | +import abbreviatePlayerPositon from '../../../../helpers/abbreviatePlayerPositon'; | |
6 | +import { | |
7 | + Column, | |
8 | + ItemRow, | |
9 | + PlayerColumn, | |
10 | + PriceWithTrendColumn | |
11 | +} from '../../../ListGrid'; | |
12 | +import { CardHeight } from '../../../player-card/constants'; | |
13 | +import { BorderRadius } from '../../constants'; | |
14 | + | |
15 | +interface Props { | |
16 | + data: PlayerEdge; | |
17 | + isOwned: boolean; | |
18 | + activeSort: string; | |
19 | + onPress: () => any; | |
20 | +} | |
21 | + | |
22 | +const styles = StyleSheet.create({ | |
23 | + itemWrapper: { | |
24 | + height: CardHeight, | |
25 | + borderRadius: BorderRadius | |
26 | + }, | |
27 | + player: { | |
28 | + justifyContent: 'center', | |
29 | + width: '100%' | |
30 | + }, | |
31 | + column: { | |
32 | + justifyContent: 'center', | |
33 | + alignItems: 'center' | |
34 | + }, | |
35 | + priceTrend: { | |
36 | + justifyContent: 'center', | |
37 | + alignItems: 'center' | |
38 | + } | |
39 | +}); | |
40 | + | |
41 | +const Item: React.FC<Props> = ({ | |
42 | + data: { node: playerNode }, | |
43 | + onPress, | |
44 | + activeSort, | |
45 | + isOwned | |
46 | +}: Props) => ( | |
47 | + <TouchableOpacity onPress={onPress}> | |
48 | + <ItemRow style={styles.itemWrapper} key={playerNode?.id}> | |
49 | + <PlayerColumn | |
50 | + isOwned={isOwned} | |
51 | + name={`${playerNode?.firstName} ${playerNode?.lastName}`} | |
52 | + position={abbreviatePlayerPositon(playerNode?.playerPositions)} | |
53 | + team={get('team.abbreviation', playerNode)} | |
54 | + rating={4.3} | |
55 | + size={4} | |
56 | + style={styles.player} | |
57 | + /> | |
58 | + <Column | |
59 | + size={2} | |
60 | + style={styles.column} | |
61 | + color={activeSort === 'rank' && 'info'} | |
62 | + > | |
63 | + {playerNode?.rank} | |
64 | + </Column> | |
65 | + <Column | |
66 | + size={2.5} | |
67 | + style={styles.column} | |
68 | + color={activeSort === 'fantasyPoints' && 'info'} | |
69 | + > | |
70 | + {playerNode?.fantasyPoints} | |
71 | + </Column> | |
72 | + <Column | |
73 | + size={2.5} | |
74 | + style={styles.column} | |
75 | + color={activeSort === 'fantasyPointsPerGame' && 'info'} | |
76 | + > | |
77 | + {playerNode?.fantasyPointsPerGame} | |
78 | + </Column> | |
79 | + <PriceWithTrendColumn | |
80 | + rise | |
81 | + size={2.3} | |
82 | + style={styles.priceTrend} | |
83 | + height={45} | |
84 | + current={playerNode?.value} | |
85 | + color={activeSort === 'value' && 'info'} | |
86 | + /> | |
87 | + </ItemRow> | |
88 | + </TouchableOpacity> | |
89 | +); | |
90 | + | |
91 | +export default Item; | ... | ... |
1 | +import { StackActions, useNavigation } from '@react-navigation/native'; | |
2 | +import { get, isEmpty } from 'lodash/fp'; | |
3 | +import { Spinner as NativeSpinner, View } from 'native-base'; | |
4 | +import React, { useCallback, useEffect, useState } from 'react'; | |
5 | +import { | |
6 | + DataProvider, | |
7 | + LayoutProvider, | |
8 | + RecyclerListView | |
9 | +} from 'recyclerlistview'; | |
10 | +import { TabNavigation } from '../../../types/navigation'; | |
11 | +import ListGrid, { HeaderRow } from '../../ListGrid'; | |
12 | +import HeaderColumn from '../../ListGrid/HeaderColumn'; | |
13 | +import { CardHeight } from '../../player-card/constants'; | |
14 | +import { COLUMNS, DEFAULT_SORT } from '../constants'; | |
15 | +import Item from './Item'; | |
16 | + | |
17 | +interface Props { | |
18 | + players: PlayerEdge[]; | |
19 | + franchisePlayers: Dictionary<FranchisePlayer>; | |
20 | + onSort: (field: string, order: string) => void; | |
21 | + onFetchMore: VoidFunction; | |
22 | + isFetching: boolean; | |
23 | +} | |
24 | + | |
25 | +const layoutProvider = new LayoutProvider( | |
26 | + () => 'MARKET_LIST', | |
27 | + (_, dim) => { | |
28 | + // eslint-disable-next-line no-param-reassign | |
29 | + dim.width = 500; | |
30 | + // eslint-disable-next-line no-param-reassign | |
31 | + dim.height = CardHeight + 1; | |
32 | + } | |
33 | +); | |
34 | + | |
35 | +const MainList: React.FC<Props> = ({ | |
36 | + isFetching, | |
37 | + onFetchMore, | |
38 | + franchisePlayers, | |
39 | + onSort, | |
40 | + players | |
41 | +}: Props) => { | |
42 | + const navigation = useNavigation<TabNavigation>(); | |
43 | + const [sortBy, setSortBy] = useState<string>(DEFAULT_SORT.COLUMN); | |
44 | + const [sortDirection, setSortDirection] = useState<SortType>( | |
45 | + DEFAULT_SORT.DIRECTION | |
46 | + ); | |
47 | + const [dataProvider, setDataProvider] = useState<DataProvider>( | |
48 | + new DataProvider((r1, r2) => r1 !== r2) | |
49 | + ); | |
50 | + | |
51 | + useEffect(() => { | |
52 | + if (sortBy && sortDirection) { | |
53 | + onSort(sortBy, sortDirection); | |
54 | + } | |
55 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
56 | + }, [sortBy, sortDirection]); | |
57 | + | |
58 | + useEffect(() => { | |
59 | + if (!isEmpty(players)) { | |
60 | + setDataProvider(dataProvider.cloneWithRows(players)); | |
61 | + } | |
62 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
63 | + }, [players]); | |
64 | + | |
65 | + const handleOnSort = (colName: string) => () => { | |
66 | + if (colName === sortBy) { | |
67 | + setSortDirection(sortDirection === 'ASC' ? 'DESC' : 'ASC'); | |
68 | + } else { | |
69 | + setSortBy(colName); | |
70 | + setSortDirection('ASC'); | |
71 | + } | |
72 | + }; | |
73 | + | |
74 | + const getColumnSortDirection = (colName: string) => | |
75 | + colName === sortBy ? sortDirection : ''; | |
76 | + | |
77 | + const renderFooter = useCallback( | |
78 | + () => (isFetching ? <NativeSpinner /> : <View />), | |
79 | + [isFetching] | |
80 | + ); | |
81 | + | |
82 | + const navigateToProfile = (selectedIndex: number) => () => { | |
83 | + navigation.dispatch( | |
84 | + StackActions.push('PlayerView', { | |
85 | + players, | |
86 | + selectedIndex, | |
87 | + franchisePlayers | |
88 | + }) | |
89 | + ); | |
90 | + }; | |
91 | + | |
92 | + const renderRow = useCallback( | |
93 | + (_, data: PlayerEdge, index: number) => { | |
94 | + return ( | |
95 | + <Item | |
96 | + data={data} | |
97 | + activeSort={sortBy} | |
98 | + isOwned={!isEmpty(get(data.node.id, franchisePlayers))} | |
99 | + onPress={navigateToProfile(index)} | |
100 | + /> | |
101 | + ); | |
102 | + }, | |
103 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
104 | + [dataProvider] | |
105 | + ); | |
106 | + | |
107 | + const hasPlayerData = dataProvider.getSize() > 0; | |
108 | + return ( | |
109 | + <ListGrid style={{ marginHorizontal: 6 }}> | |
110 | + <HeaderRow style={{ height: 45, marginBottom: 20 }}> | |
111 | + <HeaderColumn align="center" size={4} fontSize="sm" label="PLAYER" /> | |
112 | + <HeaderColumn | |
113 | + sortable | |
114 | + size={2} | |
115 | + fontSize="sm" | |
116 | + label="Rank" | |
117 | + onSort={handleOnSort(COLUMNS.RANK)} | |
118 | + sortDirection={getColumnSortDirection(COLUMNS.RANK)} | |
119 | + /> | |
120 | + <HeaderColumn | |
121 | + sortable | |
122 | + size={2.5} | |
123 | + fontSize="sm" | |
124 | + label="FP" | |
125 | + helpText="FANTASY PTS" | |
126 | + onSort={handleOnSort(COLUMNS.FP)} | |
127 | + sortDirection={getColumnSortDirection(COLUMNS.FP)} | |
128 | + /> | |
129 | + <HeaderColumn | |
130 | + sortable | |
131 | + size={2.5} | |
132 | + fontSize="sm" | |
133 | + label="FPPG" | |
134 | + helpText={'FANTASY PTS\n(per game)'} | |
135 | + onSort={handleOnSort(COLUMNS.FPPG)} | |
136 | + sortDirection={getColumnSortDirection(COLUMNS.FPPG)} | |
137 | + /> | |
138 | + <HeaderColumn | |
139 | + sortable | |
140 | + size={2.3} | |
141 | + fontSize="sm" | |
142 | + label="Price" | |
143 | + helpText={'TREND\n(24hrs)'} | |
144 | + onSort={handleOnSort(COLUMNS.PRICE)} | |
145 | + sortDirection={getColumnSortDirection(COLUMNS.PRICE)} | |
146 | + /> | |
147 | + </HeaderRow> | |
148 | + {hasPlayerData && ( | |
149 | + <RecyclerListView | |
150 | + style={{ flex: 1 }} | |
151 | + dataProvider={dataProvider} | |
152 | + layoutProvider={layoutProvider} | |
153 | + rowRenderer={renderRow} | |
154 | + initialRenderIndex={0} | |
155 | + renderAheadOffset={1500} | |
156 | + renderFooter={renderFooter} | |
157 | + onEndReached={onFetchMore} | |
158 | + onEndReachedThreshold={2000} | |
159 | + /> | |
160 | + )} | |
161 | + </ListGrid> | |
162 | + ); | |
163 | +}; | |
164 | + | |
165 | +export default MainList; | ... | ... |
1 | +import React from 'react'; | |
2 | +import { StyleSheet } from 'react-native'; | |
3 | +import ListGrid, { | |
4 | + HeaderRow, | |
5 | + ItemRow, | |
6 | + PassingColumn, | |
7 | + PlayerColumn, | |
8 | + ReceivingColumn, | |
9 | + RushingColumn | |
10 | +} from '../../ListGrid'; | |
11 | +import HeaderColumn from '../../ListGrid/HeaderColumn'; | |
12 | +import { BorderRadius } from '../constants'; | |
13 | + | |
14 | +const styles = StyleSheet.create({ | |
15 | + itemWrapper: { | |
16 | + height: 50, | |
17 | + borderRadius: BorderRadius | |
18 | + }, | |
19 | + player: { | |
20 | + justifyContent: 'center' | |
21 | + }, | |
22 | + stats: { | |
23 | + justifyContent: 'center', | |
24 | + alignItems: 'center' | |
25 | + } | |
26 | +}); | |
27 | + | |
28 | +const StatsList: React.FC<any> = () => { | |
29 | + return ( | |
30 | + <ListGrid style={{ marginTop: 10 }}> | |
31 | + <HeaderRow style={{ marginBottom: 5 }}> | |
32 | + <HeaderColumn align="center" size={3} fontSize="sm" label="Player" /> | |
33 | + <HeaderColumn | |
34 | + align="center" | |
35 | + size={2} | |
36 | + fontSize="sm" | |
37 | + label="Passing" | |
38 | + helpText="PaYD / PaTD" | |
39 | + /> | |
40 | + <HeaderColumn | |
41 | + align="center" | |
42 | + size={2.5} | |
43 | + fontSize="sm" | |
44 | + label="Rushing" | |
45 | + helpText="Att / RuYd / RuTD" | |
46 | + /> | |
47 | + <HeaderColumn | |
48 | + align="center" | |
49 | + size={2.5} | |
50 | + fontSize="sm" | |
51 | + label="Receiving" | |
52 | + helpText="Rec / ReYd / ReTD" | |
53 | + /> | |
54 | + </HeaderRow> | |
55 | + <ItemRow style={styles.itemWrapper}> | |
56 | + <PlayerColumn | |
57 | + owned | |
58 | + name="EZEKEL ELLIOT" | |
59 | + position="RB" | |
60 | + team="DAL" | |
61 | + rating={4.3} | |
62 | + size={3} | |
63 | + style={styles.player} | |
64 | + /> | |
65 | + <PassingColumn size={2} yards="0.0" td="0.0" style={styles.stats} /> | |
66 | + <RushingColumn | |
67 | + size={2.5} | |
68 | + attempts="16.3" | |
69 | + yards="87.5" | |
70 | + td="0.7" | |
71 | + style={styles.stats} | |
72 | + /> | |
73 | + <ReceivingColumn | |
74 | + size={2.5} | |
75 | + receptions="4.1" | |
76 | + yards="56.7" | |
77 | + td="0.3" | |
78 | + style={styles.stats} | |
79 | + /> | |
80 | + </ItemRow> | |
81 | + <ItemRow style={styles.itemWrapper}> | |
82 | + <PlayerColumn | |
83 | + name="MICHAEL THOMAS" | |
84 | + position="WR" | |
85 | + team="NO" | |
86 | + rating={3.5} | |
87 | + size={3} | |
88 | + style={styles.player} | |
89 | + /> | |
90 | + <PassingColumn size={2} yards="0.0" td="0.0" style={styles.stats} /> | |
91 | + <RushingColumn | |
92 | + size={2.5} | |
93 | + attempts="16.3" | |
94 | + yards="87.5" | |
95 | + td="0.7" | |
96 | + style={styles.stats} | |
97 | + /> | |
98 | + <ReceivingColumn | |
99 | + size={2.5} | |
100 | + receptions="4.1" | |
101 | + yards="56.7" | |
102 | + td="0.3" | |
103 | + style={styles.stats} | |
104 | + /> | |
105 | + </ItemRow> | |
106 | + <ItemRow style={styles.itemWrapper}> | |
107 | + <PlayerColumn | |
108 | + name="JULIO JONES" | |
109 | + position="WR" | |
110 | + team="ATL" | |
111 | + rating={4} | |
112 | + size={3} | |
113 | + style={styles.player} | |
114 | + /> | |
115 | + <PassingColumn size={2} yards="0.0" td="0.0" style={styles.stats} /> | |
116 | + <RushingColumn | |
117 | + size={2.5} | |
118 | + attempts="16.3" | |
119 | + yards="87.5" | |
120 | + td="0.7" | |
121 | + style={styles.stats} | |
122 | + /> | |
123 | + <ReceivingColumn | |
124 | + size={2.5} | |
125 | + receptions="4.1" | |
126 | + yards="56.7" | |
127 | + td="0.3" | |
128 | + style={styles.stats} | |
129 | + /> | |
130 | + </ItemRow> | |
131 | + <ItemRow style={styles.itemWrapper}> | |
132 | + <PlayerColumn | |
133 | + name="DESEAN WATSON" | |
134 | + position="QB" | |
135 | + team="HOU" | |
136 | + rating={4} | |
137 | + size={3} | |
138 | + style={styles.player} | |
139 | + /> | |
140 | + <PassingColumn size={2} yards="0.0" td="0.0" style={styles.stats} /> | |
141 | + <RushingColumn | |
142 | + size={2.5} | |
143 | + attempts="16.3" | |
144 | + yards="87.5" | |
145 | + td="0.7" | |
146 | + style={styles.stats} | |
147 | + /> | |
148 | + <ReceivingColumn | |
149 | + size={2.5} | |
150 | + receptions="4.1" | |
151 | + yards="56.7" | |
152 | + td="0.3" | |
153 | + style={styles.stats} | |
154 | + /> | |
155 | + </ItemRow> | |
156 | + <ItemRow style={styles.itemWrapper}> | |
157 | + <PlayerColumn | |
158 | + name="COOPER KUPP" | |
159 | + position="WR" | |
160 | + team="STL" | |
161 | + rating={3} | |
162 | + size={3} | |
163 | + style={styles.player} | |
164 | + /> | |
165 | + <PassingColumn size={2} yards="0.0" td="0.0" style={styles.stats} /> | |
166 | + <RushingColumn | |
167 | + size={2.5} | |
168 | + attempts="16.3" | |
169 | + yards="87.5" | |
170 | + td="0.7" | |
171 | + style={styles.stats} | |
172 | + /> | |
173 | + <ReceivingColumn | |
174 | + size={2.5} | |
175 | + receptions="4.1" | |
176 | + yards="56.7" | |
177 | + td="0.3" | |
178 | + style={styles.stats} | |
179 | + /> | |
180 | + </ItemRow> | |
181 | + </ListGrid> | |
182 | + ); | |
183 | +}; | |
184 | + | |
185 | +export default StatsList; | ... | ... |
1 | +import React from 'react'; | |
2 | +import { StyleSheet } from 'react-native'; | |
3 | +import ListGrid, { | |
4 | + FPColumn, | |
5 | + HeaderRow, | |
6 | + ItemRow, | |
7 | + PlayerColumn | |
8 | +} from '../../ListGrid'; | |
9 | +import HeaderColumn from '../../ListGrid/HeaderColumn'; | |
10 | +import { BorderRadius, CardHeight } from '../constants'; | |
11 | + | |
12 | +const styles = StyleSheet.create({ | |
13 | + itemWrapper: { | |
14 | + height: CardHeight, | |
15 | + borderRadius: BorderRadius | |
16 | + }, | |
17 | + player: { | |
18 | + justifyContent: 'center' | |
19 | + }, | |
20 | + stats: { | |
21 | + justifyContent: 'center', | |
22 | + alignItems: 'center' | |
23 | + } | |
24 | +}); | |
25 | + | |
26 | +const TotalsList: React.FC<any> = () => { | |
27 | + return ( | |
28 | + <ListGrid style={{ marginTop: 10 }}> | |
29 | + <HeaderRow style={{ marginBottom: 5 }}> | |
30 | + <HeaderColumn align="center" size={3} fontSize="sm" label="PLAYER" /> | |
31 | + <HeaderColumn align="center" size={1.5} fontSize="sm" label="1-week" /> | |
32 | + <HeaderColumn align="center" size={1.5} fontSize="sm" label="2-week" /> | |
33 | + <HeaderColumn align="center" size={1.5} fontSize="sm" label="4-week" /> | |
34 | + <HeaderColumn align="center" size={1.5} fontSize="sm" label="Season" /> | |
35 | + </HeaderRow> | |
36 | + <ItemRow style={styles.itemWrapper}> | |
37 | + <PlayerColumn | |
38 | + name="EZEKEL ELLIOT" | |
39 | + position="RB" | |
40 | + team="DAL" | |
41 | + rating={4.3} | |
42 | + size={3} | |
43 | + style={styles.player} | |
44 | + /> | |
45 | + <FPColumn size={1.7} style={styles.stats}> | |
46 | + 24.5 | |
47 | + </FPColumn> | |
48 | + <FPColumn size={1.5} style={styles.stats}> | |
49 | + 14.9 | |
50 | + </FPColumn> | |
51 | + <FPColumn size={1.5} style={styles.stats}> | |
52 | + 15.8 | |
53 | + </FPColumn> | |
54 | + <FPColumn size={1.5} style={styles.stats}> | |
55 | + 24.5 | |
56 | + </FPColumn> | |
57 | + </ItemRow> | |
58 | + <ItemRow style={styles.itemWrapper}> | |
59 | + <PlayerColumn | |
60 | + name="MICHAEL THOMAS" | |
61 | + position="WR" | |
62 | + team="NO" | |
63 | + rating={3.5} | |
64 | + size={3} | |
65 | + style={styles.player} | |
66 | + /> | |
67 | + <FPColumn size={1.5} style={styles.stats}> | |
68 | + 33.2 | |
69 | + </FPColumn> | |
70 | + <FPColumn size={1.5} style={styles.stats}> | |
71 | + 31.6 | |
72 | + </FPColumn> | |
73 | + <FPColumn size={1.5} style={styles.stats}> | |
74 | + 16.9 | |
75 | + </FPColumn> | |
76 | + <FPColumn size={1.5} style={styles.stats}> | |
77 | + 33.2 | |
78 | + </FPColumn> | |
79 | + </ItemRow> | |
80 | + <ItemRow style={styles.itemWrapper}> | |
81 | + <PlayerColumn | |
82 | + name="JULIO JONES" | |
83 | + position="WR" | |
84 | + team="ATL" | |
85 | + rating={4} | |
86 | + size={3} | |
87 | + style={styles.player} | |
88 | + /> | |
89 | + <FPColumn size={1.5} style={styles.stats}> | |
90 | + 33.2 | |
91 | + </FPColumn> | |
92 | + <FPColumn size={1.5} style={styles.stats}> | |
93 | + 17.7 | |
94 | + </FPColumn> | |
95 | + <FPColumn size={1.5} style={styles.stats}> | |
96 | + 25.9 | |
97 | + </FPColumn> | |
98 | + <FPColumn size={1.5} style={styles.stats}> | |
99 | + 33.2 | |
100 | + </FPColumn> | |
101 | + </ItemRow> | |
102 | + <ItemRow style={styles.itemWrapper}> | |
103 | + <PlayerColumn | |
104 | + name="DESEAN WATSON" | |
105 | + position="QB" | |
106 | + team="HOU" | |
107 | + rating={4} | |
108 | + size={3} | |
109 | + style={styles.player} | |
110 | + /> | |
111 | + <FPColumn size={1.5} style={styles.stats}> | |
112 | + 24.5 | |
113 | + </FPColumn> | |
114 | + <FPColumn size={1.5} style={styles.stats}> | |
115 | + 19.9 | |
116 | + </FPColumn> | |
117 | + <FPColumn size={1.5} style={styles.stats}> | |
118 | + 15.6 | |
119 | + </FPColumn> | |
120 | + <FPColumn size={1.5} style={styles.stats}> | |
121 | + 24.5 | |
122 | + </FPColumn> | |
123 | + </ItemRow> | |
124 | + <ItemRow style={styles.itemWrapper}> | |
125 | + <PlayerColumn | |
126 | + owned | |
127 | + name="COOPER KUPP" | |
128 | + position="WR" | |
129 | + team="STL" | |
130 | + rating={3} | |
131 | + size={3} | |
132 | + style={styles.player} | |
133 | + /> | |
134 | + <FPColumn size={1.5} style={styles.stats}> | |
135 | + 33.2 | |
136 | + </FPColumn> | |
137 | + <FPColumn size={1.5} style={styles.stats}> | |
138 | + 21.8 | |
139 | + </FPColumn> | |
140 | + <FPColumn size={1.5} style={styles.stats}> | |
141 | + 15.8 | |
142 | + </FPColumn> | |
143 | + <FPColumn size={1.5} style={styles.stats}> | |
144 | + 33.2 | |
145 | + </FPColumn> | |
146 | + </ItemRow> | |
147 | + </ListGrid> | |
148 | + ); | |
149 | +}; | |
150 | + | |
151 | +export default TotalsList; | ... | ... |
1 | +import React from 'react'; | |
2 | +import { ViewProps } from 'react-native'; | |
3 | +import ListGrid from '../../ListGrid'; | |
4 | +import WeeklyPlayerRow from '../../WeeklyPlayerRow'; | |
5 | + | |
6 | +interface ComponentProps { | |
7 | + data: Player; | |
8 | + active: boolean; | |
9 | +} | |
10 | + | |
11 | +type Props = ComponentProps & ViewProps; | |
12 | + | |
13 | +const AverageList: React.FC<Props> = ({ data, active, ...props }: Props) => { | |
14 | + const rowLabel = 'PPG'; | |
15 | + return ( | |
16 | + // loop here | |
17 | + <ListGrid {...props}> | |
18 | + <WeeklyPlayerRow | |
19 | + key="dummy1" | |
20 | + player={data} | |
21 | + active={active} | |
22 | + rowLabel={rowLabel} | |
23 | + /> | |
24 | + <WeeklyPlayerRow | |
25 | + key="dummy2" | |
26 | + player={data} | |
27 | + active={active} | |
28 | + rowLabel={rowLabel} | |
29 | + /> | |
30 | + <WeeklyPlayerRow | |
31 | + key="dummy3" | |
32 | + player={data} | |
33 | + active={active} | |
34 | + rowLabel={rowLabel} | |
35 | + /> | |
36 | + <WeeklyPlayerRow | |
37 | + key="dummy4" | |
38 | + player={data} | |
39 | + active={active} | |
40 | + rowLabel={rowLabel} | |
41 | + /> | |
42 | + <WeeklyPlayerRow | |
43 | + key="dummy5" | |
44 | + player={data} | |
45 | + active={active} | |
46 | + rowLabel={rowLabel} | |
47 | + /> | |
48 | + <WeeklyPlayerRow | |
49 | + key="dummy6" | |
50 | + player={data} | |
51 | + active={active} | |
52 | + rowLabel={rowLabel} | |
53 | + /> | |
54 | + <WeeklyPlayerRow | |
55 | + key="dummy7" | |
56 | + player={data} | |
57 | + active={active} | |
58 | + rowLabel={rowLabel} | |
59 | + /> | |
60 | + <WeeklyPlayerRow | |
61 | + key="dummy8" | |
62 | + player={data} | |
63 | + active={active} | |
64 | + rowLabel={rowLabel} | |
65 | + /> | |
66 | + <WeeklyPlayerRow | |
67 | + key="dummy9" | |
68 | + player={data} | |
69 | + active={active} | |
70 | + rowLabel={rowLabel} | |
71 | + /> | |
72 | + <WeeklyPlayerRow | |
73 | + key="dummy10" | |
74 | + player={data} | |
75 | + active={active} | |
76 | + rowLabel={rowLabel} | |
77 | + /> | |
78 | + <WeeklyPlayerRow | |
79 | + key="dummy11" | |
80 | + player={data} | |
81 | + active={active} | |
82 | + rowLabel={rowLabel} | |
83 | + /> | |
84 | + <WeeklyPlayerRow | |
85 | + key="dummy12" | |
86 | + player={data} | |
87 | + active={active} | |
88 | + rowLabel={rowLabel} | |
89 | + /> | |
90 | + </ListGrid> | |
91 | + ); | |
92 | +}; | |
93 | + | |
94 | +export default AverageList; | ... | ... |
1 | +import React from 'react'; | |
2 | +import { ViewProps } from 'react-native'; | |
3 | +import ListGrid from '../../ListGrid'; | |
4 | +import StatsPlayerRow from '../../StatsPlayerRow'; | |
5 | + | |
6 | +interface ComponentProps { | |
7 | + data: Player; | |
8 | + active: boolean; | |
9 | +} | |
10 | + | |
11 | +type Props = ComponentProps & ViewProps; | |
12 | + | |
13 | +const StatsList: React.FC<Props> = ({ data, active, ...props }: Props) => ( | |
14 | + <ListGrid {...props}> | |
15 | + <StatsPlayerRow key="dummy1" player={data} active={active} /> | |
16 | + <StatsPlayerRow key="dummy2" player={data} active={active} /> | |
17 | + <StatsPlayerRow key="dummy3" player={data} active={active} /> | |
18 | + <StatsPlayerRow key="dummy4" player={data} active={active} /> | |
19 | + <StatsPlayerRow key="dummy5" player={data} active={active} /> | |
20 | + <StatsPlayerRow key="dummy6" player={data} active={active} /> | |
21 | + <StatsPlayerRow key="dummy7" player={data} active={active} /> | |
22 | + <StatsPlayerRow key="dummy8" player={data} active={active} /> | |
23 | + <StatsPlayerRow key="dummy9" player={data} active={active} /> | |
24 | + <StatsPlayerRow key="dummy10" player={data} active={active} /> | |
25 | + <StatsPlayerRow key="dummy11" player={data} active={active} /> | |
26 | + <StatsPlayerRow key="dummy12" player={data} active={active} /> | |
27 | + </ListGrid> | |
28 | +); | |
29 | + | |
30 | +export default StatsList; | ... | ... |
1 | +import React from 'react'; | |
2 | +import { ViewProps } from 'react-native'; | |
3 | +import ListGrid from '../../ListGrid'; | |
4 | +import WeeklyPlayerRow from '../../WeeklyPlayerRow'; | |
5 | + | |
6 | +interface ComponentProps { | |
7 | + data: Player; | |
8 | + active: boolean; | |
9 | +} | |
10 | + | |
11 | +type Props = ComponentProps & ViewProps; | |
12 | + | |
13 | +const TotalsList: React.FC<Props> = ({ data, active, ...props }: Props) => { | |
14 | + const rowLabel = 'FP'; | |
15 | + return ( | |
16 | + // loop here | |
17 | + <ListGrid {...props}> | |
18 | + <WeeklyPlayerRow | |
19 | + key="dummy1" | |
20 | + player={data} | |
21 | + active={active} | |
22 | + rowLabel={rowLabel} | |
23 | + /> | |
24 | + <WeeklyPlayerRow | |
25 | + key="dummy2" | |
26 | + player={data} | |
27 | + active={active} | |
28 | + rowLabel={rowLabel} | |
29 | + /> | |
30 | + <WeeklyPlayerRow | |
31 | + key="dummy3" | |
32 | + player={data} | |
33 | + active={active} | |
34 | + rowLabel={rowLabel} | |
35 | + /> | |
36 | + <WeeklyPlayerRow | |
37 | + key="dummy4" | |
38 | + player={data} | |
39 | + active={active} | |
40 | + rowLabel={rowLabel} | |
41 | + /> | |
42 | + <WeeklyPlayerRow | |
43 | + key="dummy5" | |
44 | + player={data} | |
45 | + active={active} | |
46 | + rowLabel={rowLabel} | |
47 | + /> | |
48 | + <WeeklyPlayerRow | |
49 | + key="dummy6" | |
50 | + player={data} | |
51 | + active={active} | |
52 | + rowLabel={rowLabel} | |
53 | + /> | |
54 | + <WeeklyPlayerRow | |
55 | + key="dummy7" | |
56 | + player={data} | |
57 | + active={active} | |
58 | + rowLabel={rowLabel} | |
59 | + /> | |
60 | + <WeeklyPlayerRow | |
61 | + key="dummy8" | |
62 | + player={data} | |
63 | + active={active} | |
64 | + rowLabel={rowLabel} | |
65 | + /> | |
66 | + <WeeklyPlayerRow | |
67 | + key="dummy9" | |
68 | + player={data} | |
69 | + active={active} | |
70 | + rowLabel={rowLabel} | |
71 | + /> | |
72 | + <WeeklyPlayerRow | |
73 | + key="dummy10" | |
74 | + player={data} | |
75 | + active={active} | |
76 | + rowLabel={rowLabel} | |
77 | + /> | |
78 | + <WeeklyPlayerRow | |
79 | + key="dummy11" | |
80 | + player={data} | |
81 | + active={active} | |
82 | + rowLabel={rowLabel} | |
83 | + /> | |
84 | + <WeeklyPlayerRow | |
85 | + key="dummy12" | |
86 | + player={data} | |
87 | + active={active} | |
88 | + rowLabel={rowLabel} | |
89 | + /> | |
90 | + </ListGrid> | |
91 | + ); | |
92 | +}; | |
93 | + | |
94 | +export default TotalsList; | ... | ... |
ReactNative/components/RosterList/index.tsx
0 → 100644
1 | +import { get, getOr, isEqual, keyBy, sortBy, findIndex } from 'lodash/fp'; | |
2 | +import { View } from 'native-base'; | |
3 | +import React from 'react'; | |
4 | +import { ViewProps } from 'react-native'; | |
5 | +import { StackActions, useNavigation } from '@react-navigation/native'; | |
6 | +import { TouchableOpacity } from 'react-native-gesture-handler'; | |
7 | +import { ABBREV, POSITIONS } from '../../constants/roster-position'; | |
8 | +import BenchDivider from '../BenchDivider'; | |
9 | +import DefaultPlayerRow from '../DefaultPlayerRow'; | |
10 | +import ListGrid from '../ListGrid'; | |
11 | +import { TAB } from '../../constants/tab'; | |
12 | +import { TabNavigation, StackNavigation } from '../../types/navigation'; | |
13 | + | |
14 | +interface ComponentProps { | |
15 | + players: FranchisePlayer[]; | |
16 | + rosterSpots: string[]; | |
17 | + // eslint-disable-next-line react/require-default-props | |
18 | + showMarketTrend?: boolean; | |
19 | +} | |
20 | + | |
21 | +type Props = ComponentProps & ViewProps; | |
22 | + | |
23 | +const RosterList: React.FC<Props> = ({ | |
24 | + players, | |
25 | + rosterSpots, | |
26 | + showMarketTrend = false, | |
27 | + ...props | |
28 | +}: Props) => { | |
29 | + const navigation = useNavigation<TabNavigation | StackNavigation>(); | |
30 | + const benchIndex = rosterSpots.indexOf('BENCH'); | |
31 | + const franchisePlayers = keyBy('rosterSpotIndex', players); | |
32 | + const sortedPlayersSpots = sortBy('rosterSpotIndex', players); | |
33 | + | |
34 | + const navigateToProfile = (index: number) => { | |
35 | + navigation.dispatch( | |
36 | + StackActions.push('PlayerView', { | |
37 | + players: sortedPlayersSpots, | |
38 | + selectedIndex: index | |
39 | + }) | |
40 | + ); | |
41 | + }; | |
42 | + | |
43 | + const handleOnPressItem = (index: number) => () => { | |
44 | + const rosterSpotIndex = get( | |
45 | + [`${index}`, 'rosterSpotIndex'], | |
46 | + franchisePlayers | |
47 | + ); | |
48 | + const playerIndex = findIndex({ rosterSpotIndex }, sortedPlayersSpots); | |
49 | + if (playerIndex > -1) { | |
50 | + navigateToProfile(playerIndex); | |
51 | + } else { | |
52 | + const position = getOr('', rosterSpots[index], ABBREV); | |
53 | + navigation.navigate(TAB.Market, { | |
54 | + position: rosterSpots[index] !== POSITIONS.BENCH ? position : null | |
55 | + }); | |
56 | + } | |
57 | + }; | |
58 | + | |
59 | + return ( | |
60 | + <ListGrid {...props}> | |
61 | + {rosterSpots.map( | |
62 | + (position: string, index: number) => ( | |
63 | + <TouchableOpacity | |
64 | + // eslint-disable-next-line react/no-array-index-key | |
65 | + key={`player-${position}-${index}`} | |
66 | + onPress={handleOnPressItem(index)} | |
67 | + > | |
68 | + <View> | |
69 | + {index === benchIndex && <BenchDivider />} | |
70 | + <DefaultPlayerRow | |
71 | + active={!isEqual(rosterSpots[index], POSITIONS.BENCH)} | |
72 | + position={get(position, ABBREV)} | |
73 | + player={get(index, franchisePlayers)} | |
74 | + showMarketTrend={showMarketTrend} | |
75 | + /> | |
76 | + </View> | |
77 | + </TouchableOpacity> | |
78 | + ), | |
79 | + rosterSpots | |
80 | + )} | |
81 | + </ListGrid> | |
82 | + ); | |
83 | +}; | |
84 | + | |
85 | +export default RosterList; | ... | ... |
1 | +import { gql } from '@apollo/client'; | |
2 | + | |
3 | +// eslint-disable-next-line import/prefer-default-export | |
4 | +export const BUY_FRANCHISE_PLAYER = gql` | |
5 | + mutation CreateFranchisePlayers( | |
6 | + $franchiseId: String! | |
7 | + $playerId: String! | |
8 | + $rosterSpotIndex: Int! | |
9 | + ) { | |
10 | + createNflFranchisePlayer( | |
11 | + data: { | |
12 | + franchiseId: $franchiseId | |
13 | + playerId: $playerId | |
14 | + rosterSpotIndex: $rosterSpotIndex | |
15 | + } | |
16 | + ) { | |
17 | + id | |
18 | + playerId | |
19 | + franchiseId | |
20 | + rosterSpotIndex | |
21 | + buyPrice | |
22 | + } | |
23 | + } | |
24 | +`; | ... | ... |
1 | +import { gql } from '@apollo/client'; | |
2 | +import apolloClient from '../../../store/apolloClient'; | |
3 | + | |
4 | +// eslint-disable-next-line import/prefer-default-export | |
5 | +export const FIND_FRANCHISE_PLAYERS = gql` | |
6 | + query FindFranchisePlayers($franchiseId: String!) { | |
7 | + findNflFranchisePlayers(filter: { franchiseId: { eq: $franchiseId } }) { | |
8 | + nodes { | |
9 | + id | |
10 | + playerId | |
11 | + franchiseId | |
12 | + rosterSpotIndex | |
13 | + buyPrice | |
14 | + } | |
15 | + } | |
16 | + } | |
17 | +`; | |
18 | + | |
19 | +export default async (franchiseId: string) => { | |
20 | + try { | |
21 | + await apolloClient.query({ | |
22 | + query: FIND_FRANCHISE_PLAYERS, | |
23 | + variables: { franchiseId }, | |
24 | + fetchPolicy: 'network-only' | |
25 | + }); | |
26 | + return null; | |
27 | + } catch (e) { | |
28 | + return null; | |
29 | + } | |
30 | +}; | ... | ... |
1 | +import { gql } from '@apollo/client'; | |
2 | +import { get } from 'lodash/fp'; | |
3 | +import apolloClient from '../../../store/apolloClient'; | |
4 | + | |
5 | +export const FIND_USER_CURRENT_NFL_SEASON_FRANCHISE = gql` | |
6 | + query FindNflSeasonFranchises($userId: String!) { | |
7 | + currentNflSeason { | |
8 | + id | |
9 | + franchises(filter: { ownerId: { eq: $userId } }) { | |
10 | + nodes { | |
11 | + id | |
12 | + name | |
13 | + fzCash | |
14 | + } | |
15 | + } | |
16 | + } | |
17 | + } | |
18 | +`; | |
19 | + | |
20 | +export default async (userId: string) => { | |
21 | + try { | |
22 | + const { data } = await apolloClient.query({ | |
23 | + query: FIND_USER_CURRENT_NFL_SEASON_FRANCHISE, | |
24 | + variables: { userId }, | |
25 | + fetchPolicy: 'network-only' | |
26 | + }); | |
27 | + return get('currentNflSeason.franchises.nodes.0', data); | |
28 | + } catch (e) { | |
29 | + return null; | |
30 | + } | |
31 | +}; | ... | ... |
1 | +import { gql } from '@apollo/client'; | |
2 | + | |
3 | +// eslint-disable-next-line import/prefer-default-export | |
4 | +export const FIND_USER_CURRENT_NFL_SEASON_PLAYER = gql` | |
5 | + query FindCurrentNflSeasonPlayer($playerId: String!) { | |
6 | + currentNflSeason { | |
7 | + id | |
8 | + player(id: $playerId) { | |
9 | + id | |
10 | + lastName | |
11 | + firstName | |
12 | + fantasyPointsPerGame | |
13 | + playerPositions | |
14 | + value | |
15 | + details { | |
16 | + profilePhotos | |
17 | + } | |
18 | + team { | |
19 | + abbreviation | |
20 | + } | |
21 | + } | |
22 | + } | |
23 | + } | |
24 | +`; | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Command; | |
4 | + | |
5 | +use AppBundle\Entity\AdminUser; | |
6 | +use AppBundle\Entity\OpentokSessionTypes; | |
7 | +use AppBundle\Entity\Patient; | |
8 | +use AppBundle\Entity\VoiceCall; | |
9 | +use Doctrine\Bundle\DoctrineBundle\Registry; | |
10 | +use Monolog\Logger; | |
11 | +use NexmoBundle\Manager\InboundCallManager; | |
12 | +use OpenTokBundle\Pool\GlobalAdminOpenTokPool; | |
13 | +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; | |
14 | +use Symfony\Component\Console\Input\InputArgument; | |
15 | +use Symfony\Component\Console\Input\InputInterface; | |
16 | +use Symfony\Component\Console\Input\InputOption; | |
17 | +use Symfony\Component\Console\Output\OutputInterface; | |
18 | + | |
19 | +class NexmoBroadcastInboundCallCommand extends ContainerAwareCommand | |
20 | +{ | |
21 | + /** | |
22 | + * @var Logger | |
23 | + */ | |
24 | + private $logger; | |
25 | + | |
26 | + protected function configure() | |
27 | + { | |
28 | + $this | |
29 | + ->setName('nexmo:broadcast-inbound-call') | |
30 | + ->setDescription('Broadcast inbound voice call') | |
31 | + ->addArgument('voice_call', InputArgument::REQUIRED, 'Voice call id') | |
32 | + ->addOption("cobrand", "c", InputOption::VALUE_OPTIONAL, 'Cobrand code of executing context') | |
33 | + ; | |
34 | + } | |
35 | + | |
36 | + private function logString($s) | |
37 | + { | |
38 | + return "[{$this->getName()}]: {$s}"; | |
39 | + } | |
40 | + | |
41 | + /** | |
42 | + * @param InputInterface $input | |
43 | + * @param OutputInterface $output | |
44 | + * @return int|void|null | |
45 | + * @throws \Exception | |
46 | + */ | |
47 | + protected function execute(InputInterface $input, OutputInterface $output) | |
48 | + { | |
49 | + $this->logger = $this->getContainer()->get("logger"); | |
50 | + | |
51 | + /** @var InboundCallManager $manager */ | |
52 | + $manager = $this->getContainer()->get("nexmo.manager.inbound_call"); | |
53 | + /** @var VoiceCall $voiceCall */ | |
54 | + $voiceCall = $manager->getObjectOrHalt($input->getArgument('voice_call')); | |
55 | + $manager->initThreadContent($voiceCall); | |
56 | + $opentokData = $manager->asOpenTokSignalData($voiceCall); | |
57 | + | |
58 | + if ($voiceCall->getRecipient() instanceof AdminUser) { | |
59 | + /** @var GlobalAdminOpenTokPool $pool */ | |
60 | + $pool = $this->getContainer()->get("opentok_pool_factory")->get(OpentokSessionTypes::ADMIN_POOL); | |
61 | + // send opentok signal to admin user | |
62 | + $pool->sendSignal("inbound-call", $opentokData); | |
63 | + } | |
64 | + | |
65 | + $this->logger->info($this->logString("Broadcasting voice call #{$voiceCall->getId()}")); | |
66 | + } | |
67 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Controller; | |
4 | + | |
5 | +use AppBundle\Entity\Patient; | |
6 | +use AppBundle\Entity\VoiceCall; | |
7 | +use AppBundle\Event\VoiceCallEvent; | |
8 | +use AppBundle\Event\VoiceCallEvents; | |
9 | +use CobrandBundle\CobrandInstance; | |
10 | +use FOS\RestBundle\Controller\Annotations\Get; | |
11 | +use FOS\RestBundle\Controller\Annotations\Post; | |
12 | +use JMS\DiExtraBundle\Annotation\Inject; | |
13 | +use NexmoBundle\VoiceCall\VoiceCallOrigins; | |
14 | +use NexmoBundle\VoiceCall\VoiceCallStatus; | |
15 | +use NexmoBundle\WebHook\OnAnswerInboundWebHook; | |
16 | +use NexmoBundle\WebHook\OnEventInboundWebHook; | |
17 | +use Sentry\SentryBundle\SentrySymfonyClient; | |
18 | +use Symfony\Component\HttpFoundation\Request; | |
19 | +use Symfony\Component\HttpFoundation\Response; | |
20 | + | |
21 | +class InboundCallWebHookController extends NexmoWebHookController | |
22 | +{ | |
23 | + /** | |
24 | + * @var OnAnswerInboundWebHook | |
25 | + * @Inject("nexmo.webhooks.answer_inbound") | |
26 | + */ | |
27 | + private $answerHandler; | |
28 | + | |
29 | + /** | |
30 | + * @var OnEventInboundWebHook | |
31 | + * @Inject("nexmo.webhooks.event_inbound") | |
32 | + */ | |
33 | + private $eventHookHandler; | |
34 | + | |
35 | + /** | |
36 | + * Inbound call answer url | |
37 | + * @Get("/answer-url", name="answer_webhook") | |
38 | + * @param Request $request | |
39 | + * @return \Symfony\Component\HttpFoundation\JsonResponse | |
40 | + * @throws \Doctrine\DBAL\ConnectionException | |
41 | + */ | |
42 | + public function answerUrlAction(Request $request) | |
43 | + { | |
44 | + return $this->answerHandler->handle($request); | |
45 | + } | |
46 | + | |
47 | + /** | |
48 | + * @Post("/event-url", name="event_webhook") | |
49 | + * @param Request $request | |
50 | + * @return \Symfony\Component\HttpFoundation\JsonResponse | |
51 | + * @throws \Exception | |
52 | + */ | |
53 | + public function eventUrlAction(Request $request) | |
54 | + { | |
55 | + return $this->eventHookHandler->handle($request); | |
56 | + } | |
57 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Controller; | |
4 | + | |
5 | +use AppBundle\Controller\SpokeControllerTrait; | |
6 | +use AppBundle\Entity\VoiceCall; | |
7 | +use CobrandBundle\CobrandInstance; | |
8 | +use Symfony\Bundle\FrameworkBundle\Controller\Controller; | |
9 | + | |
10 | +class NexmoWebHookController extends Controller | |
11 | +{ | |
12 | + use SpokeControllerTrait; | |
13 | + | |
14 | + /** | |
15 | + * @param $s | |
16 | + * @return string | |
17 | + */ | |
18 | + protected function logString($s) | |
19 | + { | |
20 | + return "[nexmo_web_hook] => {$s}"; | |
21 | + } | |
22 | + | |
23 | + /** | |
24 | + * @return string | |
25 | + */ | |
26 | + protected function getCurrentAppName() | |
27 | + { | |
28 | + try { | |
29 | + $appName = $this->getCurrentCobrand()->getApp()->getName(); | |
30 | + } catch (\Exception $exception) { | |
31 | + $appName = "Spoke Health"; | |
32 | + } | |
33 | + | |
34 | + return $appName; | |
35 | + } | |
36 | + | |
37 | + | |
38 | + /** | |
39 | + * @param $uuid | |
40 | + * @return VoiceCall | |
41 | + */ | |
42 | + protected function getVoiceCallByUUID($uuid) | |
43 | + { | |
44 | + | |
45 | + return $this->getDoctrine()->getRepository("AppBundle:VoiceCall")->findOneBy(["uuid" => $uuid]); | |
46 | + } | |
47 | + | |
48 | + /** | |
49 | + * @param $uuid | |
50 | + * @return VoiceCall | |
51 | + * @throws \Exception | |
52 | + */ | |
53 | + protected function findOrCreateVoiceCallByUuid($uuid) | |
54 | + { | |
55 | + $voiceCall = $this->getVoiceCallByUUID($uuid); | |
56 | + if (!$voiceCall instanceof VoiceCall) { | |
57 | + $this->get("logger")->info($this->logString("Creating new VoiceCall - UUID: {$uuid}")); | |
58 | + // create voice call for this inbound call | |
59 | + $voiceCall = new VoiceCall(); | |
60 | + $voiceCall->setCreatedAt(new \DateTime("now")); | |
61 | + } | |
62 | + | |
63 | + return $voiceCall; | |
64 | + } | |
65 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Controller; | |
4 | + | |
5 | +use AppBundle\Controller\SpokeControllerTrait; | |
6 | +use AppBundle\Entity\User; | |
7 | +use AppBundle\Entity\VoiceCall; | |
8 | +use AppBundle\Event\VoiceCallEvent; | |
9 | +use AppBundle\Event\VoiceCallEvents; | |
10 | +use CobrandBundle\CobrandInstance; | |
11 | +use Doctrine\ORM\ORMException; | |
12 | +use FOS\RestBundle\Controller\Annotations\Get; | |
13 | +use FOS\RestBundle\Controller\Annotations\Post; | |
14 | + | |
15 | +use NexmoBundle\VoiceCall\VoiceCallOrigins; | |
16 | +use NexmoBundle\VoiceCall\VoiceCallStatus; | |
17 | +use Symfony\Bundle\FrameworkBundle\Controller\Controller; | |
18 | +use Symfony\Component\HttpFoundation\Request; | |
19 | +use Symfony\Component\HttpFoundation\Response; | |
20 | + | |
21 | +class OutboundCallWebHookController extends NexmoWebHookController | |
22 | +{ | |
23 | + /** | |
24 | + * @Post("/event-url", name="event_webhook") | |
25 | + * | |
26 | + * @param Request $request | |
27 | + * @return \Symfony\Component\HttpFoundation\JsonResponse | |
28 | + */ | |
29 | + public function postEventAction(Request $request) | |
30 | + { | |
31 | + $uuid = $request->get("uuid"); | |
32 | + $voiceCall = $this->getDoctrine()->getRepository("AppBundle:VoiceCall")->findOneBy([ | |
33 | + "uuid" => $uuid | |
34 | + ]); | |
35 | + | |
36 | + if (!$voiceCall instanceof VoiceCall) { | |
37 | + return $this->jsonResponse(["success" => false], Response::HTTP_OK); | |
38 | + } | |
39 | + | |
40 | + $delegates = []; | |
41 | + $requestMap = [ | |
42 | + "status" => function ($val) use ($voiceCall, &$delegates) { | |
43 | + $voiceCall->setStatus($val); | |
44 | + // add delegate for changing statuses | |
45 | + $delegates[] = function () use ($voiceCall) { | |
46 | + $event = new VoiceCallEvent($voiceCall); | |
47 | + | |
48 | + switch($voiceCall->getStatus()) { | |
49 | + case VoiceCallStatus::STARTED: | |
50 | + case VoiceCallStatus::RINGING: | |
51 | + $this->get("event_dispatcher")->dispatch(VoiceCallEvents::STARTED_VOICE_CALL, $event); | |
52 | + break; | |
53 | + case VoiceCallStatus::ANSWERED: | |
54 | + $this->get("event_dispatcher")->dispatch(VoiceCallEvents::ANSWERED_VOICE_CALL, $event); | |
55 | + break; | |
56 | + case VoiceCallStatus::BUSY: | |
57 | + case VoiceCallStatus::CANCELLED: | |
58 | + case VoiceCallStatus::REJECTED: | |
59 | + case VoiceCallStatus::FAILED: | |
60 | + $this->get("event_dispatcher")->dispatch(VoiceCallEvents::FAILED_VOICE_CALL, $event); | |
61 | + break; | |
62 | + case VoiceCallStatus::COMPLETED: | |
63 | + $this->get("event_dispatcher")->dispatch(VoiceCallEvents::COMPLETED_VOICE_CALL, $event); | |
64 | + break; | |
65 | + } | |
66 | + }; // end delegate func | |
67 | + }, | |
68 | + "timestamp" => function ($val) use ($voiceCall) { | |
69 | + $dt = new \DateTime($val); | |
70 | + $voiceCall->setUpdatedAt($dt); | |
71 | + }, | |
72 | + "rate" => function ($val) use ($voiceCall) { | |
73 | + $voiceCall->setRate($val); | |
74 | + }, | |
75 | + "price" => function ($val) use ($voiceCall) { | |
76 | + $voiceCall->setPrice($val); | |
77 | + }, | |
78 | + "duration" => function ($val) use ($voiceCall) { | |
79 | + $voiceCall->setDuration($val); | |
80 | + }, | |
81 | + ]; | |
82 | + | |
83 | + foreach ($request->request->all() as $key => $val) { | |
84 | + if (array_key_exists($key, $requestMap)) { | |
85 | + $requestMap[$key]($val); | |
86 | + } | |
87 | + } | |
88 | + | |
89 | + $em = $this->getEntityManager(); | |
90 | + $em->persist($voiceCall); | |
91 | + | |
92 | + try { | |
93 | + $em->flush(); | |
94 | + if (!empty($delegates)) { | |
95 | + foreach ($delegates as $key => $eachDelegate) { | |
96 | + $eachDelegate(); | |
97 | + } | |
98 | + } | |
99 | + | |
100 | + return $this->jsonResponse(["success" => true], Response::HTTP_OK); | |
101 | + } catch (\Exception $exception) { | |
102 | + // webhooks need 200 response | |
103 | + return $this->jsonResponse(["success" => false], Response::HTTP_OK); | |
104 | + } | |
105 | + } | |
106 | + | |
107 | + | |
108 | + /** | |
109 | + * @Get("/answer-url", name="answer_webhook") | |
110 | + * | |
111 | + * @param Request $request | |
112 | + * @return \Symfony\Component\HttpFoundation\JsonResponse | |
113 | + */ | |
114 | + public function getAnswerWebHookAction(Request $request) | |
115 | + { | |
116 | + $callerUser = $this->getDoctrine()->getRepository("AppBundle:User") | |
117 | + ->find($request->get("caller_uid", 0)); | |
118 | + | |
119 | + $eventUrl = base64_decode($request->get('event_url')); | |
120 | + | |
121 | + if ($callerUser instanceof User) { | |
122 | + $proxyNumber = (string) $callerUser->getProfile()->getPrimaryContactNumber(); | |
123 | + } | |
124 | + | |
125 | + $appName = "Spoke Health"; | |
126 | + $currentCobrand = $this->get("cobrand.factory")->current(); | |
127 | + if ($currentCobrand instanceof CobrandInstance) { | |
128 | + $appName = $currentCobrand->getApp()->getName(); | |
129 | + } | |
130 | + | |
131 | + $ncoo = [ | |
132 | + [ | |
133 | + "action" => "talk", | |
134 | + "text" => "Hi! Please stay on the line while we connect you to your assigned employee on {$appName}." | |
135 | + ], | |
136 | + [ | |
137 | + "action" => "connect", | |
138 | + "timeout" => 45, | |
139 | + "from" => $this->getParameter("nexmo_provisioned_number"), | |
140 | + "eventUrl" => [$eventUrl], | |
141 | + "endpoint" => [[ | |
142 | + "type" => "phone", | |
143 | + "number" => $proxyNumber | |
144 | + ]] | |
145 | + ] | |
146 | + ]; | |
147 | + | |
148 | + return $this->jsonResponse($ncoo, Response::HTTP_OK); | |
149 | + } | |
150 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Event\Logger\Command; | |
4 | + | |
5 | +use Chromedia\AuditLogBundle\Entity\Action; | |
6 | +use Chromedia\AuditLogBundle\Entity\ObjectName; | |
7 | +use Chromedia\AuditLogBundle\Entity\ObjectType; | |
8 | +use NexmoBundle\Event\Logger\VoiceCallAuditLogCommand; | |
9 | +use NexmoBundle\VoiceCall\VoiceCallStatus; | |
10 | + | |
11 | +class InboundVoiceCallCommand extends VoiceCallAuditLogCommand | |
12 | +{ | |
13 | + public function isProcessable() | |
14 | + { | |
15 | + return parent::isProcessable(); | |
16 | + } | |
17 | + | |
18 | + protected function buildLogData() | |
19 | + { | |
20 | + $actor = $this->voiceCallLog->getActor(); | |
21 | + $subject = $actor | |
22 | + ? $actor->getProfile()->getFullName() | |
23 | + : $this->voiceCallLog->getFromNumber(); | |
24 | + $data = [ | |
25 | + // to show in patient logs, set object id to patient, object type account | |
26 | + "objectId" => $actor ? $actor->getId() : null, | |
27 | + "objectType" => ObjectType::ACCOUNT, | |
28 | + "objectName" => ObjectName::VOICE_CALL, | |
29 | + "actionStatus" => Action::STATUS_SUCCESS, | |
30 | + "subject" => $subject | |
31 | + ]; | |
32 | + | |
33 | + // actor will be admin recipient, since activity logs should always show in admin user's context | |
34 | + if ($actor) { | |
35 | + $data["actor"] = $this->voiceCallLog->getRecipient(); | |
36 | + } | |
37 | + | |
38 | + $statusHandlers = [ | |
39 | + VoiceCallStatus::ANSWERED => function () use (&$data, $subject) { | |
40 | + $data["description"] = "{$subject} started an inbound call"; | |
41 | + $data["action"] = Action::STATUS_SUCCESS; | |
42 | + }, | |
43 | + VoiceCallStatus::COMPLETED => function () use (&$data, $subject) { | |
44 | + $data["description"] = "completed an inbound call with {$subject} with duration: ({$this->voiceCallLog->getFormattedDuration()}). "; | |
45 | + $data["action"] = Action::STATUS_SUCCESS; | |
46 | + }, | |
47 | + VoiceCallStatus::FAILED => function () use (&$data, $subject) { | |
48 | + $data["description"] = "failed to complete an inbound call with {$subject} ({$this->voiceCallLog->getFormattedDuration()})."; | |
49 | + $data["action"] = Action::STATUS_FAILURE; | |
50 | + }, | |
51 | + VoiceCallStatus::BUSY => function () use (&$data, $subject) { | |
52 | + $data["description"] = "failed to complete an inbound call with {$subject} ({$this->voiceCallLog->getFormattedDuration()})."; | |
53 | + $data["action"] = Action::STATUS_FAILURE; | |
54 | + }, | |
55 | + ]; | |
56 | + | |
57 | + if (array_key_exists($this->voiceCallLog->getStatus(), $statusHandlers)) { | |
58 | + $statusHandlers[$this->voiceCallLog->getStatus()](); | |
59 | + } | |
60 | + | |
61 | + return $data; | |
62 | + } | |
63 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Event\Logger\Command; | |
4 | + | |
5 | +use AppBundle\Entity\VoiceCall; | |
6 | +use Chromedia\AuditLogBundle\Entity\Action; | |
7 | +use Chromedia\AuditLogBundle\Entity\ObjectName; | |
8 | +use Chromedia\AuditLogBundle\Entity\ObjectType; | |
9 | +use NexmoBundle\Event\Logger\VoiceCallAuditLogCommand; | |
10 | +use NexmoBundle\VoiceCall\VoiceCallStatus; | |
11 | + | |
12 | +class UpdateVoiceCallStatusCommand extends VoiceCallAuditLogCommand | |
13 | +{ | |
14 | + | |
15 | + public function isProcessable() | |
16 | + { | |
17 | + $knownStatus = [ | |
18 | + VoiceCallStatus::STARTED, | |
19 | + VoiceCallStatus::COMPLETED, | |
20 | + VoiceCallStatus::FAILED, | |
21 | + VoiceCallStatus::BUSY | |
22 | + ]; | |
23 | + | |
24 | + return parent::isProcessable() && \in_array($this->voiceCallLog->getStatus(), $knownStatus); | |
25 | + } | |
26 | + | |
27 | + protected function buildLogData() | |
28 | + { | |
29 | + $actor = $this->voiceCallLog->getActor(); | |
30 | + $data = [ | |
31 | + // to show in patient logs, set object id to patient, object type account | |
32 | + "objectId" => $actor ? $actor->getId() : "Unknown", | |
33 | + "objectType" => ObjectType::ACCOUNT, | |
34 | + "objectName" => ObjectName::VOICE_CALL, | |
35 | + "actionStatus" => Action::STATUS_SUCCESS, | |
36 | + "subject" => $this->voiceCallLog->getActor() | |
37 | + ? $this->voiceCallLog->getActor()->getProfile()->getFullName() | |
38 | + : "Anonymous", | |
39 | + ]; | |
40 | + | |
41 | + $recipientName = $actor | |
42 | + ? $this->voiceCallLog->getActor()->getProfile()->getFullName() | |
43 | + : "Unknown"; | |
44 | + | |
45 | + $statusHandlers = [ | |
46 | + VoiceCallStatus::STARTED => function () use (&$data, $recipientName) { | |
47 | + $data["description"] = "started a voice call with {$recipientName}."; | |
48 | + $data["action"] = Action::STATUS_SUCCESS; | |
49 | + }, | |
50 | + VoiceCallStatus::COMPLETED => function () use (&$data, $recipientName) { | |
51 | + $data["description"] = "completed a voice call with {$recipientName} ({$this->voiceCallLog->getFormattedDuration()}). "; | |
52 | + $data["action"] = Action::STATUS_SUCCESS; | |
53 | + }, | |
54 | + VoiceCallStatus::FAILED => function () use (&$data, $recipientName) { | |
55 | + $data["description"] = "failed to complete a voice call with {$recipientName} ({$this->voiceCallLog->getFormattedDuration()})."; | |
56 | + $data["action"] = Action::STATUS_FAILURE; | |
57 | + }, | |
58 | + VoiceCallStatus::BUSY => function () use (&$data, $recipientName) { | |
59 | + $data["description"] = "failed to complete a voice call with {$recipientName} ({$this->voiceCallLog->getFormattedDuration()})."; | |
60 | + $data["action"] = Action::STATUS_FAILURE; | |
61 | + }, | |
62 | + ]; | |
63 | + | |
64 | + if (array_key_exists($this->voiceCallLog->getStatus(), $statusHandlers)) { | |
65 | + $statusHandlers[$this->voiceCallLog->getStatus()](); | |
66 | + } | |
67 | + | |
68 | + return $data; | |
69 | + } | |
70 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Event\Logger; | |
4 | + | |
5 | +use AppBundle\Entity\User; | |
6 | +use AppBundle\Entity\VoiceCall; | |
7 | +use AppBundle\Event\VoiceCallEvent; | |
8 | +use Symfony\Component\EventDispatcher\Event; | |
9 | + | |
10 | +abstract class VoiceCallAuditLogCommand | |
11 | +{ | |
12 | + /** | |
13 | + * @var VoiceCallEvent | |
14 | + */ | |
15 | + protected $event; | |
16 | + | |
17 | + /** | |
18 | + * @var VoiceCall | |
19 | + */ | |
20 | + protected $voiceCallLog; | |
21 | + | |
22 | + protected $logData = []; | |
23 | + | |
24 | + public function isProcessable() | |
25 | + { | |
26 | + $this->event = $this->event instanceof VoiceCallEvent ? $this->event : null; | |
27 | + $this->voiceCallLog = $this->event ? $this->event->getVoiceCall() : null; | |
28 | + | |
29 | + return $this->event && $this->voiceCallLog instanceof VoiceCall && $this->voiceCallLog->getDuration() > 0; | |
30 | + } | |
31 | + | |
32 | + abstract protected function buildLogData(); | |
33 | + | |
34 | + protected function commonLogData() | |
35 | + { | |
36 | + /** | |
37 | + * actor, currently our webhook sets caller as the employee and recipient as the employee | |
38 | + */ | |
39 | + return array( | |
40 | + "actionDateTime" => new \DateTime(), | |
41 | + "actor" => $this->event->getVoiceCall()->getRecipient() | |
42 | + ); | |
43 | + } | |
44 | + | |
45 | + final public function resolve(Event $event) | |
46 | + { | |
47 | + $this->event = $event; | |
48 | + if ($this->isProcessable()) { | |
49 | + $this->logData = array_merge($this->commonLogData(), $this->buildLogData()) ; | |
50 | + } | |
51 | + | |
52 | + return $this->logData; | |
53 | + } | |
54 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Event\Logger; | |
4 | + | |
5 | +use AppBundle\Entity\User; | |
6 | +use AppBundle\Event\VoiceCallEvents; | |
7 | +use Chromedia\AuditLogBundle\Resolver\EventResolverInterface; | |
8 | +use NexmoBundle\Event\Logger\Command\InboundVoiceCallCommand; | |
9 | +use NexmoBundle\Event\Logger\Command\UpdateVoiceCallStatusCommand; | |
10 | +use Symfony\Component\EventDispatcher\Event; | |
11 | +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; | |
12 | + | |
13 | +class VoiceCallAuditLogResolver implements EventResolverInterface | |
14 | +{ | |
15 | + protected $commands; | |
16 | + | |
17 | + /** | |
18 | + * @var TokenStorage | |
19 | + */ | |
20 | + protected $tokenStorage; | |
21 | + | |
22 | + public function __construct() | |
23 | + { | |
24 | + $this->commands = [ | |
25 | + VoiceCallEvents::COMPLETED_VOICE_CALL => UpdateVoiceCallStatusCommand::class, | |
26 | + VoiceCallEvents::FAILED_VOICE_CALL => UpdateVoiceCallStatusCommand::class, | |
27 | + VoiceCallEvents::STARTED_VOICE_CALL => UpdateVoiceCallStatusCommand::class, | |
28 | + VoiceCallEvents::INBOUND_CALL_ONGOING => InboundVoiceCallCommand::class, | |
29 | + VoiceCallEvents::INBOUND_CALL_COMPLETED => InboundVoiceCallCommand::class | |
30 | + ]; | |
31 | + } | |
32 | + | |
33 | + public function setTokenStorage(TokenStorage $v) | |
34 | + { | |
35 | + $this->tokenStorage = $v; | |
36 | + } | |
37 | + | |
38 | + /** | |
39 | + * @param Event $event | |
40 | + * @param string $eventName | |
41 | + * @return mixed | |
42 | + */ | |
43 | + public function getEventLogInfo(Event $event, $eventName) | |
44 | + { | |
45 | + if (!isset($this->commands[$eventName])) { | |
46 | + return; | |
47 | + } | |
48 | + | |
49 | + $class = $this->commands[$eventName]; | |
50 | + $command = new $class(); | |
51 | + if ($command instanceof VoiceCallAuditLogCommand) { | |
52 | + return $command->resolve($event); | |
53 | + } | |
54 | + } | |
55 | +} | ... | ... |
1 | +<?php | |
2 | +namespace NexmoBundle\Event\Logger; | |
3 | + | |
4 | +use AppBundle\Event\VoiceCallEvents; | |
5 | +use Chromedia\AuditLogBundle\Subscriber\AuditLogEventSubscriberInterface; | |
6 | + | |
7 | +class VoiceCallAuditLogSubscriber implements AuditLogEventSubscriberInterface | |
8 | +{ | |
9 | + | |
10 | + public function getSubscribedEvents() | |
11 | + { | |
12 | + return [ | |
13 | + VoiceCallEvents::STARTED_VOICE_CALL, | |
14 | + VoiceCallEvents::FAILED_VOICE_CALL, | |
15 | + VoiceCallEvents::COMPLETED_VOICE_CALL, | |
16 | + VoiceCallEvents::INBOUND_CALL_ONGOING, | |
17 | + VoiceCallEvents::INBOUND_CALL_COMPLETED | |
18 | + ]; | |
19 | + } | |
20 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | +namespace NexmoBundle\Event\Subscriber; | |
4 | + | |
5 | +use AppBundle\Entity\ThreadContent; | |
6 | +use AppBundle\Entity\ThreadContentTypes; | |
7 | +use AppBundle\Entity\VoiceCall; | |
8 | +use AppBundle\Event\VoiceCallEvent; | |
9 | +use AppBundle\Event\VoiceCallEvents; | |
10 | +use AppBundle\Service\OpentokSignalingService; | |
11 | +use Doctrine\Bundle\DoctrineBundle\Registry; | |
12 | +use Symfony\Component\EventDispatcher\EventSubscriberInterface; | |
13 | + | |
14 | +/** | |
15 | + * Class ThreadContentVoiceCallEventSubscriber | |
16 | + * | |
17 | + */ | |
18 | +class ThreadContentVoiceCallEventSubscriber implements EventSubscriberInterface | |
19 | +{ | |
20 | + /** | |
21 | + * @var Registry | |
22 | + */ | |
23 | + protected $doctrine; | |
24 | + | |
25 | + /** | |
26 | + * @var OpentokSignalingService | |
27 | + */ | |
28 | + protected $opentokSignaling; | |
29 | + | |
30 | + public function setDoctrine(Registry $v) | |
31 | + { | |
32 | + $this->doctrine = $v; | |
33 | + } | |
34 | + | |
35 | + public function setOpentokSignaling(OpentokSignalingService $v) | |
36 | + { | |
37 | + $this->opentokSignaling = $v; | |
38 | + } | |
39 | + | |
40 | + public function onStatusChange(VoiceCallEvent $event) | |
41 | + { | |
42 | + if (!$event->getVoiceCall() instanceof VoiceCall) { | |
43 | + return; | |
44 | + } | |
45 | + | |
46 | + /** @var ThreadContent $threadContent */ | |
47 | + $threadContent = $this->doctrine->getRepository("AppBundle:ThreadContent") | |
48 | + ->findOneBy(["contentId" => $event->getVoiceCall()->getId(), "type" => ThreadContentTypes::VOICE_CALL_LOG]); | |
49 | + | |
50 | + if (!$threadContent instanceof ThreadContent) { | |
51 | + return; | |
52 | + } | |
53 | + | |
54 | + | |
55 | + $this->opentokSignaling->sendSignal($threadContent->getThread(), "voice_call_update_content", ["thread_content" => $threadContent->getId()]); | |
56 | + } | |
57 | + | |
58 | + public static function getSubscribedEvents() | |
59 | + { | |
60 | + return [ | |
61 | + VoiceCallEvents::STARTED_VOICE_CALL => 'onStatusChange', | |
62 | + VoiceCallEvents::ANSWERED_VOICE_CALL => 'onStatusChange', | |
63 | + VoiceCallEvents::COMPLETED_VOICE_CALL => 'onStatusChange', | |
64 | + VoiceCallEvents::FAILED_VOICE_CALL => 'onStatusChange' | |
65 | + ]; | |
66 | + } | |
67 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | + | |
4 | +namespace NexmoBundle\Manager; | |
5 | + | |
6 | +use AppBundle\Entity\AdminUser; | |
7 | +use AppBundle\Entity\OpentokSessionTypes; | |
8 | +use AppBundle\Entity\Patient; | |
9 | +use AppBundle\Entity\ThreadContent; | |
10 | +use AppBundle\Entity\VoiceCall; | |
11 | +use AppBundle\Exception\NotExistEntityException; | |
12 | +use AppBundle\Service\Manager\ContactNumberManager; | |
13 | +use AppBundle\Service\Manager\PatientManager; | |
14 | +use AppBundle\Service\Manager\ThreadNoteManager; | |
15 | +use AppBundle\Service\ThreadContentFactory; | |
16 | +use JMS\DiExtraBundle\Annotation\Inject; | |
17 | +use JMS\DiExtraBundle\Annotation\InjectParams; | |
18 | +use JMS\DiExtraBundle\Annotation\Service; | |
19 | +use NexmoBundle\VoiceCall\VoiceCallOrigins; | |
20 | +use OpenTokBundle\Pool\GlobalAdminOpenTokPool; | |
21 | +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | |
22 | + | |
23 | +/** | |
24 | + * Class InboundCallManager | |
25 | + * @Service("nexmo.manager.inbound_call", parent="nexmo.base_manager", autowire=true, public=true) | |
26 | + */ | |
27 | +class InboundCallManager extends NexmoCallManager | |
28 | +{ | |
29 | + /** | |
30 | + * @var ThreadContentFactory | |
31 | + */ | |
32 | + private $threadContentFactory; | |
33 | + | |
34 | + /** | |
35 | + * @var PatientManager | |
36 | + */ | |
37 | + private $patientManager; | |
38 | + | |
39 | + /** | |
40 | + * @var ThreadNoteManager | |
41 | + */ | |
42 | + private $threadNoteManager; | |
43 | + | |
44 | + /** | |
45 | + * @var ContactNumberManager | |
46 | + */ | |
47 | + private $contactNumberManager; | |
48 | + | |
49 | + /** | |
50 | + * Setter Injector for PatientManager | |
51 | + * @param $patientManager | |
52 | + * @InjectParams({ | |
53 | + * "patientManager" = @Inject("spoke.manager.patient", required=false) | |
54 | + * }) | |
55 | + */ | |
56 | + public function setPatientManager(PatientManager $patientManager) | |
57 | + { | |
58 | + $this->patientManager = $patientManager; | |
59 | + } | |
60 | + | |
61 | + /** | |
62 | + * Setter Injector for ThreadContentFactory | |
63 | + * @param $threadContentFactory | |
64 | + * @InjectParams({ | |
65 | + * "threadContentFactory" = @Inject("app.service.thread_content_factory", required=true) | |
66 | + * }) | |
67 | + */ | |
68 | + public function setThreadContentFactory(ThreadContentFactory $threadContentFactory) | |
69 | + { | |
70 | + $this->threadContentFactory = $threadContentFactory; | |
71 | + } | |
72 | + | |
73 | + /** | |
74 | + * Setter injector for ThreadNoteManager | |
75 | + * @param ThreadNoteManager $manager | |
76 | + * @InjectParams({ | |
77 | + * "manager" = @Inject("spoke.manager.thread_note", required=true) | |
78 | + * }) | |
79 | + */ | |
80 | + public function setThreadNoteManager(ThreadNoteManager $manager) | |
81 | + { | |
82 | + $this->threadNoteManager = $manager; | |
83 | + } | |
84 | + | |
85 | + /** | |
86 | + * Setter injector for ContactNumberManager | |
87 | + * @param ContactNumberManager $contactNumberManager | |
88 | + * @InjectParams({ | |
89 | + * "contactNumberManager" = @Inject("spoke.manager.contactNumber", required=true) | |
90 | + * }) | |
91 | + */ | |
92 | + public function setContactNumberManager(ContactNumberManager $contactNumberManager) | |
93 | + { | |
94 | + $this->contactNumberManager = $contactNumberManager; | |
95 | + } | |
96 | + | |
97 | + public function create() | |
98 | + { | |
99 | + $voiceCall = parent::create(); | |
100 | + $voiceCall->setOriginType(VoiceCallOrigins::MEMBER_USER_INBOUND_CALL); | |
101 | + | |
102 | + return $voiceCall; | |
103 | + } | |
104 | + | |
105 | + /** | |
106 | + * Set up thread content for this voice call | |
107 | + * @param VoiceCall $voiceCall | |
108 | + * @return null | |
109 | + * @throws \Exception | |
110 | + */ | |
111 | + public function initThreadContent(VoiceCall $voiceCall) | |
112 | + { | |
113 | + // already created thread content | |
114 | + if ($voiceCall->getThreadContent()) { | |
115 | + return null; | |
116 | + } | |
117 | + $patient = $voiceCall->getActor(); | |
118 | + if ($patient instanceof Patient) { | |
119 | + $voiceCall->setThreadContent($this->threadContentFactory->createContent($patient->getThread(), $voiceCall, $patient)); | |
120 | + } | |
121 | + } | |
122 | + | |
123 | + public function asOpenTokSignalData(VoiceCall $voiceCall) | |
124 | + { | |
125 | + $actorData = [ | |
126 | + "number" => $voiceCall->getFromNumber() | |
127 | + ]; | |
128 | + $patient = $voiceCall->getActor(); | |
129 | + if ($patient instanceof Patient) { | |
130 | + $actorData = array_merge($actorData, [ | |
131 | + "id" => $patient ? $patient->getId() : 0, | |
132 | + "name" => $patient->getProfile()->getFullName(), | |
133 | + "company" => $patient->getCompany()->getName() | |
134 | + ]); | |
135 | + } | |
136 | + | |
137 | + $now = new \DateTime('now'); | |
138 | + return [ | |
139 | + "voiceCall" => $voiceCall->getId(), | |
140 | + "recipient" => $voiceCall->getRecipient() ? $voiceCall->getRecipient()->getId() : null, | |
141 | + "actor" => $actorData, | |
142 | + "isCallOngoing" => $voiceCall->getIsCallOngoing(), | |
143 | + "duration" => $voiceCall->getDuration(), | |
144 | + "runningDuration" => $now->getTimestamp()-$voiceCall->getCreatedAt()->getTimestamp(), | |
145 | + "fromNumber" => $voiceCall->getFromNumber() | |
146 | + ]; | |
147 | + } | |
148 | + | |
149 | + protected function getUpdateMappers(VoiceCall $voiceCall) | |
150 | + { | |
151 | + return [ | |
152 | + "actor" => function ($value) use ($voiceCall) { | |
153 | + $patient = $this->patientManager->getFind($value); | |
154 | + | |
155 | + if (!$patient) { | |
156 | + throw new \Exception("Invalid patient actor for voice call."); | |
157 | + } | |
158 | + | |
159 | + $voiceCallNumber = $voiceCall->getFromNumber(); | |
160 | + | |
161 | + $contactNumber = $this->contactNumberManager->create( | |
162 | + [ | |
163 | + 'number' => substr($voiceCallNumber, -10), | |
164 | + 'country_number' => substr($voiceCallNumber, 0, strlen($voiceCallNumber) % 10), | |
165 | + 'primary' => false | |
166 | + ] | |
167 | + ); | |
168 | + | |
169 | + $this->patientManager->update($patient, ['contactNumber' => $contactNumber]); | |
170 | + $voiceCall->setActor($patient); | |
171 | + $this->initThreadContent($voiceCall); | |
172 | + }, | |
173 | + // create ThreadNote for this VoiceCall | |
174 | + "notes" => function ($value) use ($voiceCall) { | |
175 | + $parentThreadContent = $voiceCall->getThreadContent(); | |
176 | + $recipient = $voiceCall->getRecipient(); | |
177 | + | |
178 | + // must have existing thread content for main voice call log, | |
179 | + // must have a recipient, automatically set as actor for notes | |
180 | + if ($parentThreadContent && $recipient) { | |
181 | + $threadNote = $this->threadNoteManager->create($value); | |
182 | + $this->threadContentFactory->createContent($parentThreadContent->getThread(), $threadNote, $recipient, $parentThreadContent); | |
183 | + } | |
184 | + } | |
185 | + ]; | |
186 | + } | |
187 | +} | ... | ... |
1 | +<?php | |
2 | +namespace NexmoBundle\Manager; | |
3 | + | |
4 | +use AppBundle\Entity\VoiceCall; | |
5 | +use AppBundle\Repository\VoiceCallRepository; | |
6 | +use AppBundle\Service\Manager\BaseManager; | |
7 | +use Doctrine\Bundle\DoctrineBundle\Registry; | |
8 | +use Doctrine\ORM\EntityManager; | |
9 | +use JMS\DiExtraBundle\Annotation\Inject; | |
10 | +use JMS\DiExtraBundle\Annotation\InjectParams; | |
11 | +use JMS\DiExtraBundle\Annotation\Service; | |
12 | +use Sentry\SentryBundle\SentrySymfonyClient; | |
13 | +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | |
14 | +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | |
15 | + | |
16 | +/** | |
17 | + * Class NexmoCallManager | |
18 | + * @Service("nexmo.base_manager", autowire=true, public=true) | |
19 | + */ | |
20 | +abstract class NexmoCallManager extends BaseManager | |
21 | +{ | |
22 | + /** | |
23 | + * @var VoiceCallRepository | |
24 | + */ | |
25 | + protected $repository; | |
26 | + | |
27 | + /** | |
28 | + * @var SentrySymfonyClient | |
29 | + */ | |
30 | + protected $sentryClient; | |
31 | + | |
32 | + /** | |
33 | + * @param VoiceCallRepository $voiceCallRepository | |
34 | + */ | |
35 | + public function __construct(VoiceCallRepository $voiceCallRepository) | |
36 | + { | |
37 | + $this->repository = $voiceCallRepository; | |
38 | + } | |
39 | + | |
40 | + /** | |
41 | + * @param SentrySymfonyClient $client | |
42 | + * @InjectParams({ | |
43 | + * "client" = @Inject("sentry.client", required=false) | |
44 | + * }) | |
45 | + */ | |
46 | + public function setSentryClient(SentrySymfonyClient $client) | |
47 | + { | |
48 | + $this->sentryClient = $client; | |
49 | + } | |
50 | + | |
51 | + /** | |
52 | + * Common query function for all Nexmo related voice call logs since most interact via uuid. | |
53 | + * @param $uuid | |
54 | + * @return VoiceCall | |
55 | + */ | |
56 | + public function findOneByUUID($uuid) | |
57 | + { | |
58 | + return $this->repository->findOneBy(["uuid" => $uuid]); | |
59 | + } | |
60 | + | |
61 | + /** | |
62 | + * @param string $uuid | |
63 | + * @return VoiceCall | |
64 | + */ | |
65 | + public function findOneByUuidOrHalt($uuid) | |
66 | + { | |
67 | + $obj = $this->findOneByUUID($uuid); | |
68 | + if (!$obj) { | |
69 | + throw new NotFoundHttpException("Invalid voice call uuid."); | |
70 | + } | |
71 | + | |
72 | + return $obj; | |
73 | + } | |
74 | + | |
75 | + abstract protected function getUpdateMappers(VoiceCall $voiceCall); | |
76 | + | |
77 | + /** | |
78 | + * @param VoiceCall $voiceCall | |
79 | + * @param array $params | |
80 | + * @throws \Exception | |
81 | + */ | |
82 | + final public function patch(VoiceCall $voiceCall, array $params = []) | |
83 | + { | |
84 | + try { | |
85 | + $this->beginTransaction(); | |
86 | + $mappers = $this->getUpdateMappers($voiceCall); | |
87 | + | |
88 | + foreach ($params as $key => $value) { | |
89 | + if (isset($mappers[$key]) && !is_null($value)) { | |
90 | + $mappers[$key]($value); | |
91 | + } | |
92 | + } | |
93 | + | |
94 | + $this->save($voiceCall); | |
95 | + $this->commit(); | |
96 | + } catch (\Exception $exception) { | |
97 | + $this->rollback(); | |
98 | + $this->sentryClient->captureException($exception); | |
99 | + throw new BadRequestHttpException("Failed to apply patch changes for VoiceCall", $exception); | |
100 | + } | |
101 | + } | |
102 | + | |
103 | + /** | |
104 | + * @return VoiceCall | |
105 | + * @throws \Exception | |
106 | + */ | |
107 | + public function create() | |
108 | + { | |
109 | + $voiceCall = new VoiceCall(); | |
110 | + $voiceCall->setCreatedAt(new \DateTime()); | |
111 | + | |
112 | + return $voiceCall; | |
113 | + } | |
114 | +} | ... | ... |
Symfony3/NexmoBundle/NexmoBundle.php
0 → 100644
1 | +services: | |
2 | + nexmo.audit_log_resolver: | |
3 | + class: NexmoBundle\Event\Logger\VoiceCallAuditLogResolver | |
4 | + | |
5 | + nexmo.audit_log_subscriber: | |
6 | + class: NexmoBundle\Event\Logger\VoiceCallAuditLogSubscriber | |
7 | + tags: | |
8 | + - { name: audit_log.event_subscriber, resolver : nexmo.audit_log_resolver } | |
\ No newline at end of file | ... | ... |
1 | +nexmo_inbound_webhooks: | |
2 | + type: annotation | |
3 | + resource: NexmoBundle\Controller\InboundCallWebHookController | |
4 | + prefix: "web-hooks/inbound-calls" | |
5 | + name_prefix: nexmo.webhooks.inbound_call_ | |
6 | + | |
7 | +nexmo_outbound_webhooks: | |
8 | + type: annotation | |
9 | + resource: NexmoBundle\Controller\OutboundCallWebHookController | |
10 | + prefix: "web-hooks/outbound-calls" | |
11 | + name_prefix: nexmo.webhooks.outbound_call_ | ... | ... |
1 | +imports: | |
2 | + - { resource: "@NexmoBundle/Resources/config/logger.yml" } | |
3 | + | |
4 | +services: | |
5 | + nexmo.voice_call_manager: | |
6 | + class: NexmoBundle\VoiceCall\VoiceCallManager | |
7 | + arguments: ["%nexmo_application_id%", "@cobrand.factory"] | |
8 | + calls: | |
9 | + - [setProvisionedNumber, ["%nexmo_provisioned_number%"]] | |
10 | + - [setRouter, ["@router"]] | |
11 | + - [initWebhookUrls, ["%base_host%"]] | |
12 | + | |
13 | + nexmo.thread_content_voice_call_subscriber: | |
14 | + class: NexmoBundle\Event\Subscriber\ThreadContentVoiceCallEventSubscriber | |
15 | + calls: | |
16 | + - [setDoctrine, ["@doctrine"]] | |
17 | + - [setOpentokSignaling, ["@app.opentok_signaling"]] | |
18 | + tags: | |
19 | + - { name: kernel.event_subscriber } | ... | ... |
1 | +<?php | |
2 | + | |
3 | + | |
4 | +namespace NexmoBundle\WebHook; | |
5 | + | |
6 | +use AppBundle\Service\ConsoleInvoker; | |
7 | +use CobrandBundle\CobrandInstance; | |
8 | +use CobrandBundle\Service\CobrandFactory; | |
9 | +use JMS\DiExtraBundle\Annotation\Inject; | |
10 | +use JMS\DiExtraBundle\Annotation\InjectParams; | |
11 | +use JMS\DiExtraBundle\Annotation\Service; | |
12 | +use Monolog\Logger; | |
13 | +use NexmoBundle\Manager\NexmoCallManager; | |
14 | +use Sentry\SentryBundle\SentrySymfonyClient; | |
15 | +use Symfony\Component\HttpFoundation\JsonResponse; | |
16 | +use Symfony\Component\HttpFoundation\Request; | |
17 | +use Symfony\Component\HttpFoundation\Response; | |
18 | +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | |
19 | + | |
20 | +/** | |
21 | + * Class NexmoWebHookHandler | |
22 | + * @Service("nexmo.webhooks.base", abstract=true, autowire=true, public=true) | |
23 | + */ | |
24 | +abstract class NexmoWebHookHandler | |
25 | +{ | |
26 | + /** | |
27 | + * @var Request | |
28 | + */ | |
29 | + protected $request; | |
30 | + | |
31 | + protected $logger; | |
32 | + | |
33 | + protected $responseData = []; | |
34 | + | |
35 | + /** | |
36 | + * @var SentrySymfonyClient | |
37 | + */ | |
38 | + protected $sentry; | |
39 | + | |
40 | + /** | |
41 | + * @var CobrandInstance | |
42 | + */ | |
43 | + protected $currentCobrand; | |
44 | + | |
45 | + /** | |
46 | + * @var ConsoleInvoker | |
47 | + */ | |
48 | + protected $consoleInvoker; | |
49 | + | |
50 | + /** | |
51 | + * @param Logger $logger | |
52 | + * @InjectParams({ | |
53 | + * "logger" = @Inject("logger", required=false) | |
54 | + * }) | |
55 | + */ | |
56 | + public function injectLogger(Logger $logger) | |
57 | + { | |
58 | + $this->logger = $logger; | |
59 | + } | |
60 | + | |
61 | + /** | |
62 | + * @param CobrandFactory $factory | |
63 | + * @InjectParams({ | |
64 | + * "factory" = @Inject("cobrand.factory", required=false) | |
65 | + * }) | |
66 | + */ | |
67 | + public function injectCurrentCobrand(CobrandFactory $factory) | |
68 | + { | |
69 | + $this->currentCobrand = $factory->current(); | |
70 | + } | |
71 | + | |
72 | + /** | |
73 | + * @param SentrySymfonyClient $client | |
74 | + * @InjectParams({ | |
75 | + * "client" = @Inject("sentry.client", required=false) | |
76 | + * }) | |
77 | + */ | |
78 | + public function injectSentryClient(SentrySymfonyClient $client) | |
79 | + { | |
80 | + $this->sentry = $client; | |
81 | + } | |
82 | + | |
83 | + /** | |
84 | + * @param ConsoleInvoker $invoker | |
85 | + * @InjectParams({ | |
86 | + * "invoker" = @Inject("spoke.console_invoker", required=false) | |
87 | + * }) | |
88 | + */ | |
89 | + public function injectConsoleInvoker(ConsoleInvoker $invoker) | |
90 | + { | |
91 | + $this->consoleInvoker = $invoker; | |
92 | + } | |
93 | + | |
94 | + /** | |
95 | + * @param Request $request | |
96 | + * @return JsonResponse | |
97 | + */ | |
98 | + final public function handle(Request $request) | |
99 | + { | |
100 | + $this->responseData = []; | |
101 | + $this->request = $request; | |
102 | + | |
103 | + try { | |
104 | + $this->processRequest(); | |
105 | + | |
106 | + return new JsonResponse($this->responseData, Response::HTTP_OK); | |
107 | + } catch (\Exception $exception) { | |
108 | + // Important: Nexmo webhooks needs 200 response code, even for error. | |
109 | + $this->sentry->captureException($exception); | |
110 | + return new JsonResponse(["error" => $exception->getMessage()], Response::HTTP_OK); | |
111 | + } | |
112 | + } | |
113 | + | |
114 | + protected function getRequestParam($key, $required = true) | |
115 | + { | |
116 | + $val = $this->request->get($key, null); | |
117 | + if ($required && is_null($val)) { | |
118 | + throw new BadRequestHttpException(sprintf('Missing required parameter for nexmo webhook: %s', $key)); | |
119 | + } | |
120 | + | |
121 | + return $val; | |
122 | + } | |
123 | + | |
124 | + protected function logInfo($string) | |
125 | + { | |
126 | + $this->logger->info("[NEXMO_WEBHOOK]: {$string}"); | |
127 | + } | |
128 | + | |
129 | + abstract protected function processRequest(); | |
130 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | + | |
4 | +namespace NexmoBundle\WebHook; | |
5 | + | |
6 | +use AppBundle\Entity\AdminUser; | |
7 | +use AppBundle\Entity\OpentokSessionTypes; | |
8 | +use AppBundle\Entity\Patient; | |
9 | +use AppBundle\Entity\VoiceCall; | |
10 | +use AppBundle\Repository\PatientRepository; | |
11 | +use AppBundle\Service\Manager\AdminUserManager; | |
12 | +use AppBundle\Service\Manager\PatientManager; | |
13 | +use JMS\DiExtraBundle\Annotation\Inject; | |
14 | +use JMS\DiExtraBundle\Annotation\InjectParams; | |
15 | +use JMS\DiExtraBundle\Annotation\Service; | |
16 | +use NexmoBundle\Manager\InboundCallManager; | |
17 | +use NexmoBundle\VoiceCall\VoiceCallStatus; | |
18 | +use OpenTokBundle\Pool\GlobalAdminOpenTokPool; | |
19 | +use OpenTokBundle\Pool\OpenTokPoolFactory; | |
20 | + | |
21 | +/** | |
22 | + * Class OnAnswerInboundWebHook | |
23 | + * @Service("nexmo.webhooks.answer_inbound", autowire=true, public=true) | |
24 | + */ | |
25 | +class OnAnswerInboundWebHook extends NexmoWebHookHandler | |
26 | +{ | |
27 | + /** | |
28 | + * @var InboundCallManager | |
29 | + */ | |
30 | + private $callManager; | |
31 | + | |
32 | + /** | |
33 | + * @var AdminUserManager | |
34 | + */ | |
35 | + private $adminUserManager; | |
36 | + | |
37 | + /** | |
38 | + * @var PatientManager | |
39 | + */ | |
40 | + private $patientManager; | |
41 | + | |
42 | + /** | |
43 | + * @var GlobalAdminOpenTokPool | |
44 | + */ | |
45 | + private $opentokPool; | |
46 | + | |
47 | + /** | |
48 | + * @param InboundCallManager $manager | |
49 | + * @InjectParams({ | |
50 | + * "manager" = @Inject("nexmo.manager.inbound_call", required=false) | |
51 | + * }) | |
52 | + */ | |
53 | + public function setInboundVoiceCallManager(InboundCallManager $manager) | |
54 | + { | |
55 | + $this->callManager = $manager; | |
56 | + } | |
57 | + | |
58 | + /** | |
59 | + * @param AdminUserManager $manager | |
60 | + * @InjectParams({ | |
61 | + * "manager" = @Inject("spoke.manager.admin_user", required=false) | |
62 | + * }) | |
63 | + */ | |
64 | + public function injectUserManager(AdminUserManager $manager) | |
65 | + { | |
66 | + $this->adminUserManager = $manager; | |
67 | + } | |
68 | + | |
69 | + /** | |
70 | + * @param PatientManager $manager | |
71 | + * @InjectParams({ | |
72 | + * "manager" = @Inject("spoke.manager.patient", required=false) | |
73 | + * }) | |
74 | + */ | |
75 | + public function injectPatientManager(PatientManager $manager) | |
76 | + { | |
77 | + $this->patientManager = $manager; | |
78 | + } | |
79 | + | |
80 | + /** | |
81 | + * @param OpenTokPoolFactory $factory | |
82 | + * @throws \Exception | |
83 | + * @InjectParams({ | |
84 | + * "factory" = @Inject("opentok_pool_factory", required=false) | |
85 | + * }) | |
86 | + */ | |
87 | + public function injectAdminPool(OpenTokPoolFactory $factory) | |
88 | + { | |
89 | + $this->opentokPool = $factory->get(OpentokSessionTypes::ADMIN_POOL); | |
90 | + } | |
91 | + | |
92 | + | |
93 | + /** | |
94 | + * @return VoiceCall|object|null | |
95 | + * @throws \Exception | |
96 | + */ | |
97 | + private function buildVoiceCallFromRequest() | |
98 | + { | |
99 | + $uuid = $this->getRequestParam('uuid'); | |
100 | + $fromNumber = $this->getRequestParam('from'); | |
101 | + $conversationId = $this->getRequestParam('conversation_uuid'); | |
102 | + | |
103 | + $voiceCall = $this->callManager->findOneByUUID($uuid); | |
104 | + // no existing voice call with uuid, create one | |
105 | + if (!$voiceCall) { | |
106 | + $voiceCall = $this->callManager->create(); | |
107 | + } | |
108 | + | |
109 | + $voiceCall->setUpdatedAt(new \DateTime()); | |
110 | + $voiceCall->setConversationUuid($conversationId); | |
111 | + $voiceCall->setFromNumber($fromNumber); | |
112 | + $voiceCall->setUuid($uuid); | |
113 | + | |
114 | + return $voiceCall; | |
115 | + } | |
116 | + | |
117 | + protected function processRequest() | |
118 | + { | |
119 | + $voiceCall = $this->buildVoiceCallFromRequest(); | |
120 | + $this->logInfo("Inbound call answer url. From number: {$voiceCall->getFromNumber()}"); | |
121 | + | |
122 | + // find mapped member based on the contact number | |
123 | + // we can only map out one patient to this voice call log | |
124 | + // for multiple patients having same contact number will be manually managed | |
125 | + $patients = $this->patientManager->findByContactNumber($voiceCall->getFromNumber()); | |
126 | + /** @var Patient $actor */ | |
127 | + $actor = 1 == count($patients) ? $patients[0] : null; | |
128 | + if ($actor) { | |
129 | + $voiceCall->setActor($actor); | |
130 | + } | |
131 | + | |
132 | + // as per Jason discussion, we will always forward the call to the default number FOR march 15 | |
133 | + $proxyNumber = $this->currentCobrand->getNexmo()->defaultNumber; | |
134 | + | |
135 | + // find AdminUser who owns this proxyNumber, can only handle one recipient | |
136 | + $adminUsers = $this->adminUserManager->findByContactNumber($proxyNumber); | |
137 | + /** @var AdminUser $adminRecipient */ | |
138 | + $adminRecipient = 1 == count($adminUsers) ? $adminUsers[0] : null; | |
139 | + if ($adminRecipient) { | |
140 | + $voiceCall->setRecipient($adminRecipient); | |
141 | + } | |
142 | + | |
143 | + $voiceCall->setToNumber($proxyNumber); | |
144 | + | |
145 | + try { | |
146 | + $this->callManager->beginTransaction(); | |
147 | + $this->callManager->save($voiceCall); | |
148 | + $this->callManager->commit(); | |
149 | + | |
150 | + // build out response data for connecting to proxy number | |
151 | + $this->responseData = [ | |
152 | + [ | |
153 | + "action" => "connect", | |
154 | + // redirect to advocate or company default number | |
155 | + "endpoint" => [[ | |
156 | + "type" => "phone", | |
157 | + "number" => $proxyNumber | |
158 | + ]], | |
159 | + "timeout" => 45 | |
160 | + ] | |
161 | + ]; | |
162 | + } catch (\Exception $exception) { | |
163 | + $appName = $this->currentCobrand ? $this->currentCobrand->getApp()->getName() : null; | |
164 | + $this->callManager->rollback(); | |
165 | + $this->sentry->captureException($exception); | |
166 | + | |
167 | + // failure | |
168 | + $this->responseData = [[ | |
169 | + "action" => "talk", | |
170 | + "text" => "Thank you for calling ${appName}, we are currently experiencing technical problems." | |
171 | + ]]; | |
172 | + } | |
173 | + } | |
174 | +} | ... | ... |
1 | +<?php | |
2 | + | |
3 | + | |
4 | +namespace NexmoBundle\WebHook; | |
5 | + | |
6 | +use AppBundle\Entity\AdminUser; | |
7 | +use AppBundle\Entity\OpentokSessionTypes; | |
8 | +use AppBundle\Event\VoiceCallEvent; | |
9 | +use AppBundle\Event\VoiceCallEvents; | |
10 | +use JMS\DiExtraBundle\Annotation\Inject; | |
11 | +use JMS\DiExtraBundle\Annotation\InjectParams; | |
12 | +use JMS\DiExtraBundle\Annotation\Service; | |
13 | +use NexmoBundle\Manager\InboundCallManager; | |
14 | +use NexmoBundle\VoiceCall\VoiceCallStatus; | |
15 | +use OpenTokBundle\Pool\GlobalAdminOpenTokPool; | |
16 | +use OpenTokBundle\Pool\OpenTokPoolFactory; | |
17 | +use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
18 | + | |
19 | +/** | |
20 | + * Class OnEventInboundWebHook | |
21 | + * @Service("nexmo.webhooks.event_inbound", autowire=true, public=true) | |
22 | + */ | |
23 | +class OnEventInboundWebHook extends NexmoWebHookHandler | |
24 | +{ | |
25 | + /** | |
26 | + * @var InboundCallManager | |
27 | + */ | |
28 | + private $callManager; | |
29 | + | |
30 | + /** | |
31 | + * @var EventDispatcherInterface | |
32 | + */ | |
33 | + private $eventDispatcher; | |
34 | + | |
35 | + /** | |
36 | + * @var GlobalAdminOpenTokPool | |
37 | + */ | |
38 | + private $opentokPool; | |
39 | + | |
40 | + /** | |
41 | + * @param EventDispatcherInterface $dispatcher | |
42 | + * @InjectParams({ | |
43 | + * "dispatcher" = @Inject("event_dispatcher", required=false) | |
44 | + * }) | |
45 | + */ | |
46 | + public function setEventDispatcher(EventDispatcherInterface $dispatcher) | |
47 | + { | |
48 | + $this->eventDispatcher = $dispatcher; | |
49 | + } | |
50 | + | |
51 | + /** | |
52 | + * @param InboundCallManager $manager | |
53 | + * @InjectParams({ | |
54 | + * "manager" = @Inject("nexmo.manager.inbound_call", required=false) | |
55 | + * }) | |
56 | + */ | |
57 | + public function setInboundVoiceCallManager(InboundCallManager $manager) | |
58 | + { | |
59 | + $this->callManager = $manager; | |
60 | + } | |
61 | + | |
62 | + /** | |
63 | + * @param OpenTokPoolFactory $factory | |
64 | + * @throws \Exception | |
65 | + * @InjectParams({ | |
66 | + * "factory" = @Inject("opentok_pool_factory", required=false) | |
67 | + * }) | |
68 | + */ | |
69 | + public function injectAdminPool(OpenTokPoolFactory $factory) | |
70 | + { | |
71 | + $this->opentokPool = $factory->get(OpentokSessionTypes::ADMIN_POOL); | |
72 | + } | |
73 | + | |
74 | + protected function processRequest() | |
75 | + { | |
76 | + $voiceCall = $this->callManager->findOneByUuidOrHalt($this->getRequestParam('uuid')); | |
77 | + | |
78 | + $requestMap = [ | |
79 | + "status" => function ($val) use ($voiceCall, &$delegates) { | |
80 | + $voiceCall->setStatus($val); | |
81 | + }, | |
82 | + "timestamp" => function ($val) use ($voiceCall) { | |
83 | + $dt = new \DateTime($val); | |
84 | + $voiceCall->setUpdatedAt($dt); | |
85 | + }, | |
86 | + "rate" => function ($val) use ($voiceCall) { | |
87 | + $voiceCall->setRate($val); | |
88 | + }, | |
89 | + "price" => function ($val) use ($voiceCall) { | |
90 | + $voiceCall->setPrice($val); | |
91 | + }, | |
92 | + "duration" => function ($val) use ($voiceCall) { | |
93 | + $voiceCall->setDuration($val); | |
94 | + }, | |
95 | + ]; | |
96 | + | |
97 | + // call event mapper | |
98 | + foreach ($this->request->request->all() as $key => $val) { | |
99 | + if (array_key_exists($key, $requestMap)) { | |
100 | + $requestMap[$key]($val); | |
101 | + } | |
102 | + } | |
103 | + | |
104 | + try { | |
105 | + $this->callManager->beginTransaction(); | |
106 | + $this->callManager->initThreadContent($voiceCall); | |
107 | + $this->callManager->save($voiceCall); | |
108 | + $this->callManager->commit(); | |
109 | + | |
110 | + if ($voiceCall->getRecipient() instanceof AdminUser) { | |
111 | + $this->opentokPool->sendSignal("inbound-call", $this->callManager->asOpenTokSignalData($voiceCall)); | |
112 | + | |
113 | + // trigger log only on completed call | |
114 | + if (VoiceCallStatus::COMPLETED === $voiceCall->getStatus()) { | |
115 | + $this->eventDispatcher->dispatch(VoiceCallEvents::INBOUND_CALL_COMPLETED, new VoiceCallEvent($voiceCall)); | |
116 | + } | |
117 | + } | |
118 | + | |
119 | + // no needed data for response | |
120 | + $this->responseData = [ | |
121 | + 'success' => true | |
122 | + ]; | |
123 | + } catch (\Exception $exception) { | |
124 | + $this->sentry->captureException($exception); | |
125 | + | |
126 | + // no needed data for response | |
127 | + $this->responseData = [ | |
128 | + 'success' => false | |
129 | + ]; | |
130 | + } | |
131 | + } | |
132 | +} | ... | ... |
android/BoothActivity.java
0 → 100644
1 | +package com.styleteq.app; | |
2 | + | |
3 | +import android.content.DialogInterface; | |
4 | +import android.content.Intent; | |
5 | +import android.os.Bundle; | |
6 | +import android.view.Menu; | |
7 | +import android.view.MenuItem; | |
8 | +import android.view.View; | |
9 | +import android.widget.TextView; | |
10 | +import android.widget.Toast; | |
11 | + | |
12 | +import com.styleteq.app.adapters.SwipeScreenAdapter; | |
13 | +import com.styleteq.app.api.STCallback; | |
14 | +import com.styleteq.app.api.STRequest; | |
15 | +import com.styleteq.app.api.ServiceGenerator; | |
16 | +import com.styleteq.app.errors.ErrorBag; | |
17 | +import com.styleteq.app.errors.ErrorsSerializer; | |
18 | +import com.styleteq.app.helpers.Logger; | |
19 | +import com.styleteq.app.helpers.STActivity; | |
20 | +import com.styleteq.app.helpers.TemporaryValues; | |
21 | +import com.styleteq.app.helpers.Utils; | |
22 | +import com.styleteq.app.models.Attachment; | |
23 | +import com.styleteq.app.models.Booth; | |
24 | +import com.styleteq.app.models.Session; | |
25 | +import com.styleteq.app.services.BoothService; | |
26 | +import com.styleteq.app.ui.ConfirmDeleteDialog; | |
27 | +import com.styleteq.app.ui.DynamicHeightViewPager; | |
28 | +import com.styleteq.app.ui.LoadingScreen; | |
29 | +import com.viewpagerindicator.CirclePageIndicator; | |
30 | + | |
31 | +import java.util.ArrayList; | |
32 | +import java.util.List; | |
33 | + | |
34 | +import butterknife.BindView; | |
35 | +import butterknife.ButterKnife; | |
36 | +import retrofit2.Call; | |
37 | +import retrofit2.Response; | |
38 | + | |
39 | +public class BoothActivity extends STActivity { | |
40 | + @BindView(R.id.booth) | |
41 | + public TextView boothName; | |
42 | + @BindView(R.id.name) | |
43 | + public TextView name; | |
44 | + @BindView(R.id.address) | |
45 | + public TextView address; | |
46 | + @BindView(R.id.view_pager) | |
47 | + public DynamicHeightViewPager pager; | |
48 | + @BindView(R.id.indicator) | |
49 | + public CirclePageIndicator indicator; | |
50 | + @BindView(R.id.price) | |
51 | + public TextView price; | |
52 | + @BindView(R.id.method) | |
53 | + public TextView method; | |
54 | + @BindView(R.id.businessType) | |
55 | + public TextView businessType; | |
56 | + @BindView(R.id.boothContainer) | |
57 | + public View boothContainer; | |
58 | + | |
59 | + private ConfirmDeleteDialog deleteDialog; | |
60 | + private Booth booth; | |
61 | + private LoadingScreen loadingScreen; | |
62 | + private Menu menu; | |
63 | + | |
64 | + @Override | |
65 | + protected void onCreate(Bundle savedInstanceState) { | |
66 | + super.onCreate(savedInstanceState); | |
67 | + Intent i = getIntent(); | |
68 | + | |
69 | + setContentView(R.layout.activity_booth); | |
70 | + ButterKnife.bind(this); | |
71 | + | |
72 | + if (savedInstanceState != null) { | |
73 | + String s = savedInstanceState.getString("booth"); | |
74 | + if (s != null) | |
75 | + booth = Booth.createFromJsonString(s); | |
76 | + } | |
77 | + if (booth == null) { | |
78 | + if (i.hasExtra("booth")) { | |
79 | + booth = Booth.createFromJsonString(i.getStringExtra("booth")); | |
80 | + } else if (i.hasExtra("booth_id")) { | |
81 | + BoothService s = ServiceGenerator.createService(BoothService.class, "booth"); | |
82 | + new STRequest<>(s.get(i.getLongExtra("booth_id", 0)), new STCallback<Booth>() { | |
83 | + @Override | |
84 | + public void onCustomResponse(Call<Booth> call, Response<Booth> response) { | |
85 | + if (response.isSuccessful()) { | |
86 | + booth = response.body(); | |
87 | + populate(); | |
88 | + } else | |
89 | + showResponseError(response); | |
90 | + } | |
91 | + | |
92 | + @Override | |
93 | + public void onFailure(Call<Booth> call, Throwable t) { | |
94 | + Toast.makeText(BoothActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show(); | |
95 | + } | |
96 | + }).execute(); | |
97 | + } else | |
98 | + Logger.e("Booth not found"); | |
99 | + } | |
100 | + init(); | |
101 | + addListeners(); | |
102 | + if (booth != null) { | |
103 | + populate(); | |
104 | + } | |
105 | + } | |
106 | + | |
107 | + private void init() { | |
108 | + setToolbar(R.id.tool_bar); | |
109 | + loadingScreen = new LoadingScreen(); | |
110 | + loadingScreen.show(true); | |
111 | + } | |
112 | + | |
113 | + private void populate() { | |
114 | + boothName.setText(String.format("%s %s for Rent", booth.slots, getResources().getQuantityString(R.plurals.booths, booth.slots))); | |
115 | + name.setText(booth.salon.name); | |
116 | + address.setText(booth.salon.address()); | |
117 | + | |
118 | + List<Attachment> images = booth.images.isEmpty() ? booth.salon.images : booth.images; | |
119 | + if (images.isEmpty()) { | |
120 | + images = new ArrayList<>(); | |
121 | + images.add(new Attachment()); | |
122 | + } | |
123 | + | |
124 | + SwipeScreenAdapter a = new SwipeScreenAdapter(this, images, pager); | |
125 | + pager.setAdapter(a); | |
126 | + | |
127 | + indicator.setViewPager(pager); | |
128 | + price.setText(String.format("%s %s", Utils.formatDollar(booth.price.doubleValue()), booth.terms)); | |
129 | + method.setText(booth.payMethod()); | |
130 | + businessType.setText(booth.salon.businessType); | |
131 | + | |
132 | + deleteDialog = new ConfirmDeleteDialog(this, "Are you sure you want to delete this Booth?", | |
133 | + new DialogInterface.OnClickListener() { | |
134 | + public void onClick(DialogInterface dialog, int whichButton) { | |
135 | + showProgressDialogWithAction("Deleting booth"); | |
136 | + BoothService s = ServiceGenerator.createService(BoothService.class); | |
137 | + new STRequest<>(s.delete(booth.id), new STCallback<Booth>() { | |
138 | + @Override | |
139 | + public void onCustomResponse(Call<Booth> call, Response<Booth> response) { | |
140 | + hideProgressDialog(); | |
141 | + if (response.isSuccessful()) { | |
142 | + if (!getIntent().hasExtra(Utils.BACK)) { | |
143 | + TemporaryValues.setBooth(booth); | |
144 | + TemporaryValues.setMode(TemporaryValues.DELETE_MODE); | |
145 | + finish(); | |
146 | + } else | |
147 | + startActivity(new Intent(BoothActivity.this, BoothListActivity.class).putExtra("salon", booth.salon.toJsonString()).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); | |
148 | + Toast.makeText(BoothActivity.this, "Successfully deleted booth!", Toast.LENGTH_LONG).show(); | |
149 | + } else { | |
150 | + ErrorBag e = ErrorsSerializer.parseResourceErrors(response); | |
151 | + if (e != null) | |
152 | + Toast.makeText(BoothActivity.this, e.concatenateErrors(), Toast.LENGTH_LONG).show(); | |
153 | + } | |
154 | + } | |
155 | + | |
156 | + @Override | |
157 | + public void onFailure(Call<Booth> call, Throwable t) { | |
158 | + hideProgressDialog(); | |
159 | + Toast.makeText(BoothActivity.this, t.getMessage(), Toast.LENGTH_SHORT).show(); | |
160 | + } | |
161 | + }).execute(); | |
162 | + } | |
163 | + }); | |
164 | + | |
165 | + setupMenu(); | |
166 | + loadingScreen.show(false); | |
167 | + } | |
168 | + | |
169 | + private void addListeners() { | |
170 | + boothContainer.setOnClickListener(new View.OnClickListener() { | |
171 | + @Override | |
172 | + public void onClick(View v) { | |
173 | + startActivity(new Intent(BoothActivity.this, SalonActivity.class) | |
174 | + .putExtra("salon", booth.salon.toJsonString()).putExtra(Utils.BACK, Utils.BACK)); | |
175 | + } | |
176 | + }); | |
177 | + } | |
178 | + | |
179 | + @Override | |
180 | + public boolean onCreateOptionsMenu(Menu menu) { | |
181 | + this.menu = menu; | |
182 | + if (booth != null) | |
183 | + setupMenu(); | |
184 | + return true; | |
185 | + } | |
186 | + | |
187 | + private void setupMenu() { | |
188 | + if (menu == null) | |
189 | + return; | |
190 | + if (Session.isCurrentUser(booth.salon.userId)) { | |
191 | + getMenuInflater().inflate(R.menu.menu_salon, menu); | |
192 | + menu.findItem(R.id.edit).setTitle("Edit Booth"); | |
193 | + menu.findItem(R.id.manage_photos).setTitle("Manage Images"); | |
194 | + } else | |
195 | + getMenuInflater().inflate(R.menu.menu_report, menu); | |
196 | + } | |
197 | + | |
198 | + @Override | |
199 | + public boolean onOptionsItemSelected(MenuItem item) { | |
200 | + if (!Session.get().exists()) { | |
201 | + STApplication.promptLogin(); | |
202 | + return true; | |
203 | + } | |
204 | + switch (item.getItemId()) { | |
205 | + case R.id.edit: | |
206 | + startActivity(new Intent(this, BoothFormActivity.class).putExtra("booth", booth.toJsonString())); | |
207 | + return true; | |
208 | + case R.id.manage_photos: | |
209 | + startActivity(new Intent(this, UploadBoothPhotosActivity.class).putExtra("booth", booth.toJsonString()).putExtra(Utils.BACK, Utils.BACK)); | |
210 | + return true; | |
211 | + case R.id.delete: | |
212 | + deleteDialog.show(); | |
213 | + return true; | |
214 | + case R.id.report: | |
215 | + startActivity(new Intent(this, ReportActivity.class).putExtra("booth", booth.toJsonString())); | |
216 | + return true; | |
217 | + default: | |
218 | + return super.onOptionsItemSelected(item); | |
219 | + } | |
220 | + } | |
221 | + | |
222 | + @Override | |
223 | + public void onBackPressed() { | |
224 | + if (!loadingScreen.isLoading()) { | |
225 | + TemporaryValues.setBooth(booth); | |
226 | + super.onBackPressed(); | |
227 | + } | |
228 | + } | |
229 | + | |
230 | + @Override | |
231 | + public void onResume() { | |
232 | + super.onResume(); | |
233 | + Booth tempBooth = TemporaryValues.useBooth(); | |
234 | + if (tempBooth != null) { | |
235 | + booth = tempBooth; | |
236 | + init(); | |
237 | + populate(); | |
238 | + } | |
239 | + } | |
240 | + | |
241 | + @Override | |
242 | + protected void onSaveInstanceState(Bundle outState) { | |
243 | + super.onSaveInstanceState(outState); | |
244 | + if (booth != null) | |
245 | + outState.putString("booth", booth.toJsonString()); | |
246 | + } | |
247 | +} | ... | ... |
android/MyScheduleFragment.java
0 → 100644
1 | +package com.styleteq.app.fragments; | |
2 | + | |
3 | +import android.app.Activity; | |
4 | +import android.os.Bundle; | |
5 | + | |
6 | +import androidx.annotation.Nullable; | |
7 | +import androidx.fragment.app.Fragment; | |
8 | +import androidx.recyclerview.widget.LinearLayoutManager; | |
9 | +import androidx.recyclerview.widget.RecyclerView; | |
10 | + | |
11 | +import android.view.LayoutInflater; | |
12 | +import android.view.View; | |
13 | +import android.view.ViewGroup; | |
14 | +import android.view.animation.AnimationUtils; | |
15 | +import android.widget.CompoundButton; | |
16 | +import android.widget.RelativeLayout; | |
17 | +import android.widget.Switch; | |
18 | +import android.widget.Toast; | |
19 | + | |
20 | +import com.styleteq.app.R; | |
21 | +import com.styleteq.app.adapters.SchedulesAdapter; | |
22 | +import com.styleteq.app.api.STCallback; | |
23 | +import com.styleteq.app.api.STRequest; | |
24 | +import com.styleteq.app.api.ServiceGenerator; | |
25 | +import com.styleteq.app.errors.ErrorBag; | |
26 | +import com.styleteq.app.errors.ErrorsSerializer; | |
27 | +import com.styleteq.app.models.InfiniteScrollable; | |
28 | +import com.styleteq.app.models.PaginatedResult; | |
29 | +import com.styleteq.app.models.Schedule; | |
30 | +import com.styleteq.app.services.ScheduleService; | |
31 | +import com.styleteq.app.ui.DividerItemDecorator; | |
32 | +import com.styleteq.app.ui.InfiniteScrollView; | |
33 | +import com.styleteq.app.ui.LoadingScreen; | |
34 | +import com.styleteq.app.ui.NoEntityPlaceholder; | |
35 | + | |
36 | +import java.util.Collections; | |
37 | +import java.util.List; | |
38 | +import java.util.Objects; | |
39 | + | |
40 | +import butterknife.ButterKnife; | |
41 | +import retrofit2.Call; | |
42 | +import retrofit2.Response; | |
43 | + | |
44 | +/** | |
45 | + * A simple {@link Fragment} subclass. | |
46 | + * create an instance of this fragment. | |
47 | + */ | |
48 | +public abstract class MyScheduleFragment extends Fragment implements InfiniteScrollable { | |
49 | + | |
50 | + ScheduleService scheduleService; | |
51 | + boolean qWithPast; | |
52 | + | |
53 | + protected ViewGroup container; | |
54 | + protected Runnable runnable; | |
55 | + protected PaginatedResult currentResult; | |
56 | + | |
57 | + private RelativeLayout fragmentWrapper; | |
58 | + private NoEntityPlaceholder placeholder; | |
59 | + private LoadingScreen loadingScreen; | |
60 | + private boolean loading; | |
61 | + private boolean resetList; | |
62 | + | |
63 | + public MyScheduleFragment() { | |
64 | + // Required empty public constructor | |
65 | + } | |
66 | + | |
67 | + @Override | |
68 | + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | |
69 | + // Inflate the layout for this fragment | |
70 | + this.container = container; | |
71 | + return inflater.inflate(R.layout.fragment_my_schedule_list, container, false); | |
72 | + } | |
73 | + | |
74 | + @Override | |
75 | + public void onActivityCreated(@Nullable Bundle savedInstanceState) { | |
76 | + super.onActivityCreated(savedInstanceState); | |
77 | + ButterKnife.bind(Objects.requireNonNull(getFragmentActivity())); | |
78 | + preInit(); | |
79 | + init(); | |
80 | + scheduleService = ServiceGenerator.createService(ScheduleService.class); | |
81 | + addListeners(); | |
82 | + qWithPast = defaultSwitchState(); | |
83 | + } | |
84 | + | |
85 | + @Override | |
86 | + public void onResume() { | |
87 | + super.onResume(); | |
88 | + this.loadingScreen = new LoadingScreen(container); | |
89 | + placeholder.setTarget(fragmentWrapper); | |
90 | + load(0); | |
91 | + } | |
92 | + | |
93 | + @Override | |
94 | + public void onStop() { | |
95 | + super.onStop(); | |
96 | + getSchedulesAdapter().clearAll(); | |
97 | + } | |
98 | + | |
99 | + @Override | |
100 | + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { | |
101 | + super.onViewStateRestored(savedInstanceState); | |
102 | + resetList = true; | |
103 | + } | |
104 | + | |
105 | + private void init() { | |
106 | + View v = getMainView(); | |
107 | + RecyclerView recyclerView = v.findViewById(R.id.schedule_list_recycler); | |
108 | + InfiniteScrollView scrollView = v.findViewById(R.id.inf_scroll_view); | |
109 | + fragmentWrapper = v.findViewById(R.id.schedule_list); | |
110 | + placeholder = v.findViewById(R.id.placeholder); | |
111 | + | |
112 | + scrollView.setScrollable(this); | |
113 | + | |
114 | + placeholder.set(Objects.requireNonNull(getActivity()), R.string.schedule_no_title, R.string.schedule_no_sub); | |
115 | + | |
116 | + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); | |
117 | + recyclerView.setAnimation(AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); | |
118 | + recyclerView.setAdapter(getSchedulesAdapter()); | |
119 | + recyclerView.setNestedScrollingEnabled(false); | |
120 | + recyclerView.addItemDecoration(new DividerItemDecorator(Objects.requireNonNull(getActivity()), DividerItemDecorator.VERTICAL_LIST)); | |
121 | + } | |
122 | + | |
123 | + protected void addListeners() { | |
124 | + if (null != getSwitch()) { | |
125 | + getSwitch().setChecked(defaultSwitchState()); | |
126 | + getSwitch().setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { | |
127 | + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { | |
128 | + qWithPast = isChecked; | |
129 | + resetList = true; | |
130 | + load(0); | |
131 | + } | |
132 | + }); | |
133 | + } | |
134 | + } | |
135 | + | |
136 | + protected void load(int page) { | |
137 | + this.loadingScreen.show(true); | |
138 | + this.loading = true; | |
139 | + new STRequest<>(getQuery(page), new STCallback<PaginatedResult>() { | |
140 | + @Override | |
141 | + public void onCustomResponse(Call<PaginatedResult> call, Response<PaginatedResult> response) { | |
142 | + if (response.isSuccessful()) { | |
143 | + currentResult = response.body(); | |
144 | + if (null != currentResult && !currentResult.schedules.isEmpty()) { | |
145 | + getResults(currentResult.schedules); | |
146 | + } else { | |
147 | + getResults(Collections.emptyList()); | |
148 | + } | |
149 | + } else { | |
150 | + ErrorBag b = ErrorsSerializer.parseResourceErrors(response); | |
151 | + if (b != null) | |
152 | + Toast.makeText(getActivity(), b.concatenateErrors(), Toast.LENGTH_SHORT).show(); | |
153 | + } | |
154 | + checkList(); | |
155 | + endLoading(); | |
156 | + } | |
157 | + | |
158 | + @Override | |
159 | + public void onFailure(Call<PaginatedResult> call, Throwable t) { | |
160 | + endLoading(); | |
161 | + placeholder.showError(t.getMessage()); | |
162 | + } | |
163 | + }).execute(); | |
164 | + } | |
165 | + | |
166 | + private void checkList() { | |
167 | + if (getSchedulesAdapter().getItemCount() > 0) { | |
168 | + placeholder.show(false); | |
169 | + } else { | |
170 | + getSchedulesAdapter().notifyDataSetChanged(); | |
171 | + placeholder.show(true); | |
172 | + } | |
173 | + } | |
174 | + | |
175 | + private void getResults(List<Schedule> schedules) { | |
176 | + if (resetList) { | |
177 | + getSchedulesAdapter().clearAll(); | |
178 | + resetList = false; | |
179 | + } | |
180 | + getSchedulesAdapter().addAll(schedules); | |
181 | + } | |
182 | + | |
183 | + private void endLoading() { | |
184 | + this.loading = false; | |
185 | + this.loadingScreen.show(false); | |
186 | + } | |
187 | + | |
188 | + @Override | |
189 | + public boolean canLoad() { | |
190 | + return !this.loading && (null != currentResult && currentResult.hasNext()); | |
191 | + } | |
192 | + | |
193 | + @Override | |
194 | + public Runnable loadMoreResults() { | |
195 | + if (runnable == null) | |
196 | + runnable = new Runnable() { | |
197 | + @Override | |
198 | + public void run() { | |
199 | + load(currentResult.nextPage()); | |
200 | + } | |
201 | + }; | |
202 | + return runnable; | |
203 | + } | |
204 | + | |
205 | + protected boolean defaultSwitchState() { | |
206 | + return false; | |
207 | + } | |
208 | + | |
209 | + protected Switch getSwitch() { | |
210 | + return null; | |
211 | + } | |
212 | + | |
213 | + protected abstract void preInit(); | |
214 | + | |
215 | + protected abstract Call<PaginatedResult> getQuery(int page); | |
216 | + | |
217 | + protected abstract SchedulesAdapter getSchedulesAdapter(); | |
218 | + | |
219 | + protected abstract View getMainView(); | |
220 | + | |
221 | + protected abstract Activity getFragmentActivity(); | |
222 | + | |
223 | + protected abstract String getSource(); | |
224 | +} | ... | ... |
android/STApplication.java
0 → 100644
1 | +package com.styleteq.app; | |
2 | + | |
3 | +import android.content.ComponentName; | |
4 | +import android.content.Context; | |
5 | +import android.content.Intent; | |
6 | +import androidx.annotation.LayoutRes; | |
7 | +import androidx.multidex.MultiDexApplication; | |
8 | +import android.view.LayoutInflater; | |
9 | +import android.view.View; | |
10 | + | |
11 | +import com.crashlytics.android.Crashlytics; | |
12 | +import com.crashlytics.android.core.CrashlyticsCore; | |
13 | +import com.facebook.FacebookSdk; | |
14 | +import com.facebook.appevents.AppEventsLogger; | |
15 | +import com.styleteq.app.firebase.STFirebase; | |
16 | +import com.styleteq.app.helpers.STActivity; | |
17 | +import com.styleteq.app.helpers.Utils; | |
18 | +import com.styleteq.app.models.Session; | |
19 | + | |
20 | +import io.fabric.sdk.android.Fabric; | |
21 | + | |
22 | +public class STApplication extends MultiDexApplication { | |
23 | + private static boolean activityVisible; | |
24 | + private static STApplication instance; | |
25 | + private STActivity currentActivity; | |
26 | + | |
27 | + public static STApplication getInstance() { | |
28 | + return instance; | |
29 | + } | |
30 | + | |
31 | + public static Context applicationContext() { | |
32 | + return instance.getApplicationContext(); | |
33 | + } | |
34 | + | |
35 | + public static STActivity getCurrentActivity() { | |
36 | + return getInstance().getActivity(); | |
37 | + } | |
38 | + | |
39 | + @Override | |
40 | + public void onCreate() { | |
41 | + super.onCreate(); | |
42 | + instance = this; | |
43 | + // Set up Crashlytics, disabled for debug builds | |
44 | + Crashlytics crashlyticsKit = new Crashlytics.Builder() | |
45 | + .core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build()) | |
46 | + .build(); | |
47 | + | |
48 | + // Initialize Fabric with the debug-disabled crashlytics. | |
49 | + Fabric.with(this, crashlyticsKit); | |
50 | + STFirebase.subscribe(); | |
51 | + FacebookSdk.sdkInitialize(getApplicationContext()); | |
52 | + AppEventsLogger.activateApp(this); | |
53 | + Session.get().setNotificationId(0); | |
54 | + } | |
55 | + | |
56 | + public void setActivity(STActivity activity) { | |
57 | + currentActivity = activity; | |
58 | + } | |
59 | + | |
60 | + public STActivity getActivity() { | |
61 | + return currentActivity; | |
62 | + } | |
63 | + | |
64 | + public static void newActivity(Class klass) { | |
65 | + getCurrentActivity().startActivity(new Intent(getCurrentActivity(), klass)); | |
66 | + } | |
67 | + | |
68 | + public static void newActivity(Intent i) { | |
69 | + getCurrentActivity().startActivity(i); | |
70 | + } | |
71 | + | |
72 | + public static void newActivity(Class klass, Intent i) { | |
73 | + i.setComponent(new ComponentName(getCurrentActivity(), klass)); | |
74 | + getCurrentActivity().startActivity(i); | |
75 | + } | |
76 | + | |
77 | + public static LayoutInflater getLayoutInflater() { | |
78 | + return (LayoutInflater) getInstance().getSystemService(Context.LAYOUT_INFLATER_SERVICE); | |
79 | + } | |
80 | + | |
81 | + public static View inflate(@LayoutRes int resource) { | |
82 | + return getLayoutInflater().inflate(resource, null); | |
83 | + } | |
84 | + | |
85 | + public static boolean isActivityVisible() { | |
86 | + return activityVisible; | |
87 | + } | |
88 | + | |
89 | + public static void activityResumed() { | |
90 | + activityVisible = true; | |
91 | + Session.get().startSession(); | |
92 | + } | |
93 | + | |
94 | + public static void activityPaused() { | |
95 | + activityVisible = false; | |
96 | + Session.get().pauseSession(); | |
97 | + } | |
98 | + | |
99 | + public static void promptLogin() { | |
100 | + newActivity(new Intent(STApplication.getCurrentActivity(), LoginActivity.class).putExtra(Utils.PROMPT, true)); | |
101 | + } | |
102 | +} | ... | ... |
android/Style.java
0 → 100644
1 | +package com.styleteq.app.models; | |
2 | + | |
3 | +import android.text.TextUtils; | |
4 | + | |
5 | +import com.styleteq.app.helpers.ImageHelper; | |
6 | +import com.styleteq.app.mixpanel.MixPanelModel; | |
7 | +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | |
8 | +import com.fasterxml.jackson.annotation.JsonInclude; | |
9 | +import com.fasterxml.jackson.annotation.JsonProperty; | |
10 | + | |
11 | +import org.json.JSONObject; | |
12 | + | |
13 | +import java.util.ArrayList; | |
14 | +import java.util.List; | |
15 | + | |
16 | +@JsonInclude(JsonInclude.Include.NON_NULL) | |
17 | +@JsonIgnoreProperties(ignoreUnknown = true) | |
18 | +public class Style extends Model implements Likeable, Mappable, MixPanelModel { | |
19 | + public int id; | |
20 | + public String name; | |
21 | + public List<Attachment> images; | |
22 | + public String description; | |
23 | + | |
24 | + @JsonProperty("user") | |
25 | + public User owner; | |
26 | + | |
27 | + @JsonProperty("tag_list") | |
28 | + public String[] tagList; | |
29 | + | |
30 | + @JsonProperty("like_count") | |
31 | + public int likeCount; | |
32 | + | |
33 | + @JsonProperty("comment_count") | |
34 | + public int commentCount; | |
35 | + | |
36 | + @Override | |
37 | + public int likeCount() { | |
38 | + return likeCount; | |
39 | + } | |
40 | + | |
41 | + @JsonProperty("primary_picture_url") | |
42 | + public String primaryPictureUrl; | |
43 | + | |
44 | + @JsonProperty("user_id") | |
45 | + public long userId; | |
46 | + | |
47 | + public float latitude; | |
48 | + public float longitude; | |
49 | + | |
50 | + public String primaryPictureUrl(int style) { | |
51 | + return ImageHelper.getImageUrl(primaryPictureUrl, style); | |
52 | + } | |
53 | + | |
54 | + @JsonProperty("images_attributes") | |
55 | + public List<ImageAttributes> imageAttributes; | |
56 | + | |
57 | + @Override | |
58 | + public long id() { | |
59 | + return id; | |
60 | + } | |
61 | + | |
62 | + public static Style createFromJsonString(String json) { | |
63 | + return createFromJsonString(json, Style.class); | |
64 | + } | |
65 | + | |
66 | + public boolean is_liked; | |
67 | + | |
68 | + @Override | |
69 | + public boolean isLiked() { | |
70 | + return is_liked; | |
71 | + } | |
72 | + | |
73 | + @Override | |
74 | + public void setLiked(boolean liked) { | |
75 | + this.is_liked = liked; | |
76 | + } | |
77 | + | |
78 | + @Override | |
79 | + public void setLikeCount(int count) { | |
80 | + this.likeCount = count; | |
81 | + } | |
82 | + | |
83 | + public String tagHashString() { | |
84 | + if (tagList == null) | |
85 | + return null; | |
86 | + | |
87 | + List<String> hash = new ArrayList<>(); | |
88 | + for (String s : tagList) { | |
89 | + hash.add("#" + s); | |
90 | + } | |
91 | + return TextUtils.join(", ", hash); | |
92 | + } | |
93 | + | |
94 | + @Override | |
95 | + public MapPosition mapPosition() { | |
96 | + MapPosition p = new MapPosition(); | |
97 | + p.latitude = latitude; | |
98 | + p.longitude = longitude; | |
99 | + p.label = tagHashString(); | |
100 | + return p; | |
101 | + } | |
102 | + | |
103 | + @Override | |
104 | + public JSONObject mixpanelObject() { | |
105 | + return toJson(); | |
106 | + } | |
107 | +} | ... | ... |
graphQL/airline.rb
0 → 100644
1 | +module Queries | |
2 | + class Airline < Base | |
3 | + def define_fields | |
4 | + field :airline, Types::Airline do | |
5 | + description 'Get Airline' | |
6 | + argument :id, types.Int | |
7 | + resolve ->(obj, args, ctx) do | |
8 | + ::Airline.find_by(id: args.id) | |
9 | + end | |
10 | + end | |
11 | + field :airlines, Types::PaginatedCollection do | |
12 | + description 'Get Airlines' | |
13 | + argument :q, types.String | |
14 | + argument :page, types.Int | |
15 | + argument :per, types.Int | |
16 | + argument :sort, types.String | |
17 | + argument :includeInactive, types.Boolean | |
18 | + resolve ->(obj, args, ctx) do | |
19 | + Services::Airline::Search.run(args, ctx) | |
20 | + end | |
21 | + end | |
22 | + end | |
23 | + end | |
24 | +end | ... | ... |
swift/ActivityFeedNavigationController.swift
0 → 100644
1 | +// | |
2 | +// ActivityFeedNavigationController.swift | |
3 | +// styleteqios | |
4 | +// | |
5 | +// Created by Allejo Chris Velarde on 10/13/16. | |
6 | +// Copyright © 2016 Allejo Chris Velarde. All rights reserved. | |
7 | +// | |
8 | + | |
9 | +import Foundation | |
10 | +import UIKit | |
11 | + | |
12 | +class ActivityFeedNavigationController: UINavigationController, AuthenticatedController | |
13 | +{ | |
14 | + | |
15 | +} | |
16 | + | |
17 | +extension ActivityFeedNavigationController: LoginControllerDelegate { | |
18 | + func didSuccessfulLogin() { | |
19 | + self.loadDefaultAuthenticatedView(nil) | |
20 | + } | |
21 | +} | ... | ... |
swift/ActivityTableViewCell.swift
0 → 100644
1 | +// | |
2 | +// ActivityTableViewCell.swift | |
3 | +// styleteqios | |
4 | +// | |
5 | +// Created by Alfonz Montelibano on 4/20/16. | |
6 | +// Copyright © 2016 Allejo Chris Velarde. All rights reserved. | |
7 | +// | |
8 | + | |
9 | +import UIKit | |
10 | + | |
11 | +protocol ActivityTableViewCellDelegate { | |
12 | + func shouldRefreshCell(_ cell: ActivityTableViewCell) | |
13 | + | |
14 | + func pushController(_ viewController: UIViewController) | |
15 | + func shouldRedirectToProfile(_ username: String) | |
16 | + func didTapHashTag(_ hashtag: String) | |
17 | +} | |
18 | + | |
19 | +class ActivityTableViewCell: UITableViewCell { | |
20 | + | |
21 | + @IBOutlet weak var primaryImageHeight: NSLayoutConstraint! | |
22 | + @IBOutlet weak var activityUserImage: UIImageView! | |
23 | + @IBOutlet weak var activityImage: UIImageView! | |
24 | + @IBOutlet weak var activityUserNameLabel: UILabel! | |
25 | + @IBOutlet weak var activityDescriptionLabel: UILabel! | |
26 | + @IBOutlet weak var activityDateLabel: UILabel! | |
27 | + | |
28 | + @IBOutlet weak var numberOfLIkesUIView: UIView! | |
29 | + @IBOutlet weak var numberOfLikesUIButton: UIButton! | |
30 | + | |
31 | + var delegate: ActivityTableViewCellDelegate? | |
32 | + | |
33 | + var activityItem: Activity? | |
34 | + var adjustedSize: CGSize? | |
35 | + | |
36 | + override func awakeFromNib() { | |
37 | + super.awakeFromNib() | |
38 | + self.activityDateLabel.isHidden = true | |
39 | + } | |
40 | + | |
41 | + override func prepareForReuse() { | |
42 | + self.resetCommonElements() | |
43 | + self.primaryImageHeight.constant = 0 | |
44 | + | |
45 | + } | |
46 | + | |
47 | + func resetCommonElements() | |
48 | + { | |
49 | + self.activityUserImage.image = UIImage(named: "default-image") | |
50 | + self.activityDateLabel.text = "" | |
51 | + self.activityUserNameLabel.text = "" | |
52 | + self.activityDescriptionLabel.text = "" | |
53 | + | |
54 | + } | |
55 | + | |
56 | + func loadDefaultImage() { | |
57 | + | |
58 | + guard let targetImageView = self.activityImage else { | |
59 | + return | |
60 | + } | |
61 | + | |
62 | + let defaultImage = UIImage(named: "default-image-transparent") | |
63 | + targetImageView.image = defaultImage | |
64 | + targetImageView.contentMode = .scaleAspectFill | |
65 | + // set default size to square | |
66 | + self.primaryImageHeight.constant = UIScreen.main.bounds.size.width | |
67 | + | |
68 | + } | |
69 | + | |
70 | + | |
71 | + open func loadActivityItem(_ activityItem: Activity){ | |
72 | + self.activityItem = activityItem | |
73 | + loadCommonDetails() | |
74 | + | |
75 | + if (self.activityItem as? NotificationEntity) != nil { | |
76 | + self.primaryImageHeight.constant = 0 | |
77 | + } | |
78 | + | |
79 | + // display and update number of likes of a photo | |
80 | + if let activityType = self.activityItem?.activityType.rawValue { | |
81 | + switch activityType { | |
82 | + case "Style": | |
83 | + let style = self.activityItem?.object as! Style | |
84 | + self.updateLikesCountAndIcon(isLiked: style.isLikedByCurrentUser) | |
85 | + self.numberOfLIkesUIView.isHidden = false | |
86 | + case "Special": | |
87 | + return | |
88 | + default: | |
89 | + self.numberOfLIkesUIView.isHidden = true | |
90 | + } | |
91 | + | |
92 | + } | |
93 | + | |
94 | + if activityItem.withImage { | |
95 | + | |
96 | + self.activityImage.contentMode = .scaleAspectFit | |
97 | + self.primaryImageHeight.constant = 0 | |
98 | + WebImageHelper.instance.loadFromUrlString((self.activityItem?.imageUrl)!, imageView: self.activityImage) { (loadedImage) in | |
99 | + guard let targetImage = loadedImage else { | |
100 | + return | |
101 | + } | |
102 | + | |
103 | + let adjustedSize = targetImage.getScreenAspectSize(UIScreen.main.bounds.size) | |
104 | + self.primaryImageHeight.constant = adjustedSize.height | |
105 | + } | |
106 | + } | |
107 | + else { | |
108 | + | |
109 | + self.loadDefaultImage() | |
110 | + | |
111 | + | |
112 | + } | |
113 | + } | |
114 | + | |
115 | + func loadCommonDetails() { | |
116 | + | |
117 | + let usernameTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.presentUserProfile(tapGestureRecognizer:))) | |
118 | + let userImageTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.presentUserProfile(tapGestureRecognizer:))) | |
119 | + | |
120 | + // Set activity details | |
121 | + self.activityUserNameLabel.text = self.activityItem?.getUserName() | |
122 | + self.activityUserNameLabel?.isUserInteractionEnabled = true | |
123 | + self.activityUserNameLabel.addGestureRecognizer(usernameTapRecognizer) | |
124 | + self.activityUserImage.backgroundColor = UIColor.white | |
125 | + | |
126 | + if let rawDescription = self.activityItem?.description, let rawDateString = self.activityItem?.getDate() { | |
127 | + let attributedDescription = NSMutableAttributedString(string: "\(rawDescription) ", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12)]) | |
128 | + attributedDescription.append(NSAttributedString(string: rawDateString, attributes: [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.lightGray])) | |
129 | + self.activityDescriptionLabel.attributedText = attributedDescription | |
130 | + } | |
131 | + else { | |
132 | + self.activityDescriptionLabel.text = "" | |
133 | + } | |
134 | + | |
135 | + | |
136 | + // Set the profile pic image of the user/owner of this activity | |
137 | + if let url = self.activityItem?.getUserImageUrl() { | |
138 | + if url.isEmpty == false { | |
139 | + WebImageHelper.instance.loadFromUrlString(url, imageView: self.activityUserImage) { _ in | |
140 | + | |
141 | + self.activityUserImage?.isUserInteractionEnabled = true | |
142 | + self.activityUserImage.addGestureRecognizer(userImageTapRecognizer) | |
143 | + } | |
144 | + } | |
145 | + } | |
146 | + else { | |
147 | + self.activityUserImage.image = #imageLiteral(resourceName: "default-icon") | |
148 | + self.activityUserImage?.isUserInteractionEnabled = false | |
149 | + self.activityUserNameLabel?.isUserInteractionEnabled = false | |
150 | + } | |
151 | + } | |
152 | + | |
153 | + @objc func presentUserProfile(tapGestureRecognizer: UITapGestureRecognizer) | |
154 | + { | |
155 | + if let userId = self.activityItem?.getUserId() { | |
156 | + | |
157 | + self.activityUserNameLabel.isUserInteractionEnabled = false | |
158 | + self.activityUserImage.isUserInteractionEnabled = false | |
159 | + | |
160 | + UserRepository.instance.findById(userId) { (user, error) in | |
161 | + | |
162 | + self.activityUserNameLabel.isUserInteractionEnabled = true | |
163 | + self.activityUserImage.isUserInteractionEnabled = true | |
164 | + | |
165 | + let storyboard = UIStoryboard(name: "Search", bundle: nil) | |
166 | + let controller = storyboard.instantiateViewController(withIdentifier: "SearchProfileDetailController") as! SearchProfileDetailController | |
167 | + | |
168 | + controller.user = user | |
169 | + self.delegate?.pushController(controller) | |
170 | + } | |
171 | + } | |
172 | + | |
173 | + } | |
174 | + | |
175 | + @IBAction func pressLikeButton(_ sender: AnyObject) { | |
176 | + | |
177 | + let currentUser = UserRepository.instance.getCurrentLocalUser() | |
178 | + let style = self.activityItem?.object as! Style | |
179 | + | |
180 | + if !style.isLikedByCurrentUser { | |
181 | + currentUser.likeUpload(style) { result, error in | |
182 | + self.updateLikesCountAndIcon(isLiked: true) | |
183 | + } | |
184 | + } else { | |
185 | + currentUser.unlikeUpload(style) { result, error in | |
186 | + self.updateLikesCountAndIcon(isLiked: false) | |
187 | + } | |
188 | + } | |
189 | + } | |
190 | + | |
191 | + func updateLikesCountAndIcon(isLiked: Bool) { | |
192 | + | |
193 | + if isLiked == true { | |
194 | + self.numberOfLikesUIButton.setImage(UIImage(named: "fa-heart"), for: UIControl.State.normal) | |
195 | + } else { | |
196 | + self.numberOfLikesUIButton.setImage(UIImage(named: "fa-heart-o-white"), for: UIControl.State.normal) | |
197 | + } | |
198 | + | |
199 | + (self.activityItem?.object as! Style).getLikesCount() { count, error in | |
200 | + self.numberOfLikesUIButton.setTitle(" \(count) \( (count == 1) ? "Like" : "Likes")", for: UIControl.State.normal) | |
201 | + } | |
202 | + } | |
203 | + | |
204 | +} | |
205 | + | |
206 | + | |
207 | + | ... | ... |
swift/ActivityTableViewController.swift
0 → 100644
1 | +// | |
2 | +// ActivityTableViewController.swift | |
3 | +// styleteqios | |
4 | +// | |
5 | +// Created by Alfonz Montelibano on 5/10/16. | |
6 | +// Copyright © 2016 Allejo Chris Velarde. All rights reserved. | |
7 | +// | |
8 | + | |
9 | +import UIKit | |
10 | + | |
11 | +// This is a parent class extended by MainActivityViewController (activity feeds screen) | |
12 | +// and NotificationsTableViewController (notifications screen) | |
13 | + | |
14 | +class ActivityTableViewController: UIViewController, WithProcessIndicatorController { | |
15 | + | |
16 | + let activityTableViewCellIdentifier = "activityTableViewCell" | |
17 | + let specialActivityTableViewCellIdentifier = "specialActivityTableViewCell" | |
18 | + var activityItems: [Activity] = [] | |
19 | + | |
20 | + var adjustedHeights: [Int:CGFloat] = [:] | |
21 | + | |
22 | + | |
23 | + // Used for pagination | |
24 | + var currentPage: Int = 0 | |
25 | + var emptyCollectionLabel = UILabel() | |
26 | + var refreshControl: UIRefreshControl! | |
27 | + | |
28 | + // Process Indicator | |
29 | + var indicator = UIActivityIndicatorView() | |
30 | + var processIndicator: UIActivityIndicatorView { | |
31 | + get { | |
32 | + return self.indicator | |
33 | + } | |
34 | + } | |
35 | + | |
36 | + @IBOutlet weak var tableView: UITableView! | |
37 | + | |
38 | + override func viewWillAppear(_ animated: Bool) { | |
39 | + super.viewWillAppear(animated) | |
40 | + | |
41 | + self.clear() | |
42 | + self.refreshControl?.beginRefreshing() | |
43 | + self.fetchActivities() | |
44 | + | |
45 | + tableView.reloadData() | |
46 | + } | |
47 | + | |
48 | + | |
49 | + | |
50 | + override func viewDidLoad() { | |
51 | + super.viewDidLoad() | |
52 | + | |
53 | + | |
54 | + | |
55 | + self.view.backgroundColor = Theme.defaultBackgroundColor() | |
56 | + | |
57 | + self.registerCellNibs() | |
58 | + | |
59 | + self.tableView.tableFooterView = UIView() | |
60 | + self.tableView.tableHeaderView = UIView() | |
61 | + | |
62 | + self.emptyCollectionLabel = UILabel(frame: CGRect(x: 20, y: 0, width: self.view.bounds.width - 40, height: self.view.bounds.height)) | |
63 | + self.emptyCollectionLabel.textAlignment = .center | |
64 | + self.emptyCollectionLabel.numberOfLines = 0 | |
65 | + self.emptyCollectionLabel.isHidden = true | |
66 | + self.view.addSubview(self.emptyCollectionLabel) | |
67 | + | |
68 | + self.activityItems = [Activity]() | |
69 | + | |
70 | + self.initProcessIndicator(self.view) | |
71 | + | |
72 | + self.refreshControl = UIRefreshControl() | |
73 | + self.refreshControl?.addTarget(self, action: #selector(ActivityTableViewController.handleRefresh(_:)), for: UIControl.Event.valueChanged) | |
74 | + self.refreshControl?.tintColor = UIColor.lightGray | |
75 | + self.tableView.addSubview(self.refreshControl) | |
76 | + | |
77 | + | |
78 | + } | |
79 | + | |
80 | + func registerCellNibs() | |
81 | + { | |
82 | + self.tableView.register(UINib(nibName: "ActivityTableViewCell", bundle: nil), forCellReuseIdentifier: self.activityTableViewCellIdentifier) | |
83 | + self.tableView.register(UINib(nibName: "ActivitySpecialCellView", bundle: nil), forCellReuseIdentifier: self.specialActivityTableViewCellIdentifier) | |
84 | + self.tableView.register(UINib(nibName: "StyleActivityTableViewCell", bundle: nil), forCellReuseIdentifier: "styleActivityTableViewCell") | |
85 | + } | |
86 | + | |
87 | + func fetchActivities(){ | |
88 | + | |
89 | + } | |
90 | + | |
91 | + @objc func handleRefresh(_ refreshControl: UIRefreshControl) { | |
92 | + self.clear() | |
93 | + self.fetchActivities() | |
94 | + | |
95 | + } | |
96 | + | |
97 | + func clear() | |
98 | + { | |
99 | + if nil != self.tableView { | |
100 | + self.activityItems.removeAll() | |
101 | + self.tableView.reloadData() | |
102 | + self.currentPage = 1 | |
103 | + } | |
104 | + } | |
105 | + | |
106 | + | |
107 | +} | |
108 | + | |
109 | +extension ActivityTableViewController: UITableViewDelegate, UITableViewDataSource | |
110 | +{ | |
111 | + // MARK: Table View Delegate Functions | |
112 | + | |
113 | + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | |
114 | + | |
115 | + let activityItem = self.activityItems[indexPath.row] | |
116 | + | |
117 | + switch activityItem.activityType{ | |
118 | + case .Style: | |
119 | + self.loadStylePublicView(activityItem.object as! Style) | |
120 | + break | |
121 | + | |
122 | + case .Special: | |
123 | + self.loadSpecialPublicView(activityItem.object as! Special) | |
124 | + break | |
125 | + | |
126 | + case .Booth: | |
127 | + self.loadBoothPublicView(activityItem.object as! Booth) | |
128 | + break | |
129 | + | |
130 | + case .User: | |
131 | + self.loadUserProfilePublicView(activityItem.object as! User) | |
132 | + break | |
133 | + case .Salon: | |
134 | + self.loadSalonPublicView(activityItem.object as! Salon) | |
135 | + case .Page: | |
136 | + self.loadPublicNotificationAnnouncementPage(activityItem.object as! NotificationAnnouncementPage) | |
137 | + break | |
138 | + case .Booking: | |
139 | + guard let booking = activityItem.object as? Booking else { | |
140 | + break | |
141 | + } | |
142 | + self.loadBookingDetailView(booking) | |
143 | + break | |
144 | + default: | |
145 | + break | |
146 | + } | |
147 | + } | |
148 | + | |
149 | + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { | |
150 | + if self.isLastIndexPath(indexPath) { | |
151 | + self.fetchActivities() | |
152 | + } | |
153 | + } | |
154 | + | |
155 | + | |
156 | + | |
157 | + | |
158 | + func isLastIndexPath(_ indexPath: IndexPath) -> Bool { | |
159 | + let lastSectionIndex = self.tableView.numberOfSections - 1 | |
160 | + let lastItemIndex = self.tableView.numberOfRows(inSection: lastSectionIndex) - 1 | |
161 | + | |
162 | + return (lastSectionIndex == indexPath.section && lastItemIndex == indexPath.row) | |
163 | + } | |
164 | + | |
165 | + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
166 | + let activity = self.activityItems[indexPath.row] | |
167 | + | |
168 | + switch activity.activityType { | |
169 | + case .Special: | |
170 | + let cell = tableView.dequeueReusableCell(withIdentifier: self.specialActivityTableViewCellIdentifier) as! ActivitySpecialCellView | |
171 | + cell.loadActivityItem(activity) | |
172 | + cell.loadSpecial(activity.object as! Special) | |
173 | + cell.delegate = self | |
174 | + return cell | |
175 | + | |
176 | + case .Style: | |
177 | + let cell = tableView.dequeueReusableCell(withIdentifier: "styleActivityTableViewCell") as! StyleActivityTableViewCell | |
178 | + cell.loadActivityItem(activity) | |
179 | + cell.delegate = self | |
180 | + | |
181 | + return cell | |
182 | + default: | |
183 | + let cell = tableView.dequeueReusableCell(withIdentifier: activityTableViewCellIdentifier) as! ActivityTableViewCell | |
184 | + cell.loadActivityItem(activity) | |
185 | + cell.delegate = self | |
186 | + | |
187 | + return cell | |
188 | + } | |
189 | + | |
190 | + | |
191 | + } | |
192 | + | |
193 | + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
194 | + return self.activityItems.count | |
195 | + } | |
196 | + | |
197 | + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { | |
198 | + let activity = self.activityItems[indexPath.row] | |
199 | + switch activity.activityType { | |
200 | + case .Special: | |
201 | + return 160 | |
202 | + case .Style: | |
203 | + guard let preloadedSize = activity.carouselImageAspectSize, let itemStyle = activity.object as? Style else { | |
204 | + return UIScreen.main.bounds.size.width + 100 | |
205 | + } | |
206 | + | |
207 | + let labelHeight = itemStyle.description.height(withConstrainedWidth: UIScreen.main.bounds.width, UIFont.systemFont(ofSize: 14)) | |
208 | + | |
209 | + return preloadedSize.height + 64 + labelHeight | |
210 | + default: | |
211 | + | |
212 | + if (activity as? NotificationEntity) != nil { | |
213 | + return 80 | |
214 | + } | |
215 | + | |
216 | + if activity.withImage == false { | |
217 | + return UIScreen.main.bounds.size.width + 100 | |
218 | + } | |
219 | + else { | |
220 | + guard let preloadedSize = activity.carouselImageAspectSize else { | |
221 | + return UIScreen.main.bounds.size.width + 100 | |
222 | + } | |
223 | + | |
224 | + return preloadedSize.height + 64 | |
225 | + } | |
226 | + | |
227 | + } | |
228 | + | |
229 | + } | |
230 | + | |
231 | + | |
232 | +} | |
233 | + | |
234 | +extension ActivityTableViewController: ActivityTableViewCellDelegate { | |
235 | + func shouldRefreshCell(_ cell: ActivityTableViewCell) { | |
236 | + } | |
237 | + | |
238 | + func pushController(_ viewController: UIViewController) { | |
239 | + self.navigationController?.pushViewController(viewController, animated: true) | |
240 | + } | |
241 | + func shouldRedirectToProfile(_ username: String) { | |
242 | + UserRepository.instance.findByUsername(username) { (user, error) in | |
243 | + if let user = user { | |
244 | + self.loadUserProfilePublicView(user) | |
245 | + } else { | |
246 | + self.showAlert("Alert!", message: "User not found!", action: nil) | |
247 | + } | |
248 | + } | |
249 | + } | |
250 | + func didTapHashTag(_ hashtag: String) { | |
251 | + self.loadHashTagSearchResult(hashtag) | |
252 | + } | |
253 | + | |
254 | +} | ... | ... |
swift/MainActivityViewController.swift
0 → 100644
1 | +// | |
2 | +// MainActivityViewController.swift | |
3 | +// styleteqios | |
4 | +// | |
5 | +// Created by Alfonz Montelibano on 4/20/16. | |
6 | +// Copyright © 2016 Allejo Chris Velarde. All rights reserved. | |
7 | +// | |
8 | + | |
9 | +import UIKit | |
10 | + | |
11 | +class MainActivityViewController: ActivityTableViewController { | |
12 | + | |
13 | + // Fetch news feed activities. This func is called from ActivityTableViewController.updateList() | |
14 | + override func fetchActivities() { | |
15 | + | |
16 | + if currentPage == 0 { | |
17 | + return | |
18 | + } | |
19 | + | |
20 | + self.emptyCollectionLabel.text = "No Feeds Available. Follow a user or shop and get their latest updates." | |
21 | + self.startProcessIndicator() | |
22 | + ActivityRepository.instance.getActivityByUser(UserRepository.instance.getCurrentLocalUser(), params: ["page": self.currentPage as Any]) { (response, error) in | |
23 | + | |
24 | + self.stopProcessIndicator() | |
25 | + self.refreshControl!.endRefreshing() | |
26 | + | |
27 | + if error != nil { | |
28 | + self.showAlert("Ooops", message: "Unable to load your feeds right now.", action: nil) | |
29 | + } | |
30 | + else { | |
31 | + guard let pager = response else { | |
32 | + return | |
33 | + } | |
34 | + self.currentPage = pager.nextPage | |
35 | + | |
36 | + for each in pager.collection as! [Activity] { | |
37 | + self.activityItems.append(each) | |
38 | + self.tableView.reloadData() | |
39 | + | |
40 | + } | |
41 | + } | |
42 | + | |
43 | + self.emptyCollectionLabel.isHidden = self.activityItems.isEmpty ? false : true | |
44 | + | |
45 | + } | |
46 | + } | |
47 | +} | |
48 | + | |
49 | +extension MainActivityViewController: StyleFullViewDelegate { | |
50 | + func didUpdateStyle(_ style: Style) { | |
51 | + | |
52 | + } | |
53 | + | |
54 | + func didDeleteStyle(_ style: Style) { | |
55 | + self.activityItems = self.activityItems.filter { each in | |
56 | + | |
57 | + guard let asStyle = each.object as? Style else { | |
58 | + return true | |
59 | + } | |
60 | + | |
61 | + return (style.id == asStyle.id) ? false : true | |
62 | + } | |
63 | + self.tableView?.reloadData() | |
64 | + } | |
65 | +} | |
66 | + | |
67 | + | ... | ... |