| null
4 |
5 | export interface ILinkedRefs {
6 | [key: string]: (element: LinkRefArgumentType) => any
7 | }
8 |
9 | export interface ILinkedComponent extends Component
{
10 | _linkedRefs: ILinkedRefs
11 | }
12 |
13 | export default function linkRef(
14 | component: ILinkedComponent,
15 | name: string
16 | ) {
17 | let cache = component._linkedRefs || (component._linkedRefs = {})
18 | return (
19 | cache[name] ||
20 | (cache[name] = (element: LinkRefArgumentType) => {
21 | component[name] = element
22 | })
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/stores/API/API.ts:
--------------------------------------------------------------------------------
1 | // import { ApolloClient } from 'apollo-client'
2 | // import { DocumentNode } from 'graphql'
3 | import {
4 | AbstractAPI,
5 | StateType,
6 | AbstractTinderAPI,
7 | IAPIGenericReturn,
8 | PersonType,
9 | IAPISendMessage,
10 | AbstractFB
11 | } from '~/shared/definitions'
12 |
13 | // import * as logoutMutation from './logout.graphql'
14 | // import * as getFBQuery from './getFB.graphql'
15 | // import * as showWindow from './showWindow.graphql'
16 | // import * as loginFB from './loginFB.graphql'
17 |
18 | // import {
19 | // GetFbQuery,
20 | // ShowWindowMutation,
21 | // LoginFbMutation,
22 | // LogoutMutation
23 | // } from '~/schema'
24 | import {
25 | VIEW_MATCHES,
26 | VIEW_AUTH,
27 | routes,
28 | success,
29 | PENDING,
30 | FAILURE,
31 | SUCCESS,
32 | IPC_LOGOUT,
33 | IPC_SHOW_WINDOW
34 | } from '~/shared/constants'
35 | import { ipcRenderer } from 'electron'
36 | import { isOnline } from './utils'
37 | import * as uuid from 'uuid'
38 | import { MessageType } from '~/shared/definitions'
39 |
40 | export interface IAPIProps {
41 | state: StateType
42 | tinder: AbstractTinderAPI
43 | fb: AbstractFB
44 | }
45 |
46 | export class API implements AbstractAPI {
47 | // private client: ApolloClient
48 | private reloginPromise: Promise | null = null
49 | private state: StateType
50 | private tinder: AbstractTinderAPI
51 | private fb: AbstractFB
52 |
53 | constructor(props: IAPIProps) {
54 | Object.assign(this, props)
55 | }
56 |
57 | // mutate = async (mutation: DocumentNode, variables?: Object) => {
58 | // return (await this.client.mutate({ mutation, variables })).data as R
59 | // }
60 |
61 | // query = async (query: DocumentNode, variables?: Object) => {
62 | // return (await this.client.query({ query, variables })).data
63 | // }
64 |
65 | public login = async (silent: boolean): Promise => {
66 | this.tinder.resetClient()
67 | // let fb = (await this.query(getFBQuery)).fb
68 | try {
69 | if (this.fb.token === undefined || this.fb.id === undefined) {
70 | throw new Error('fbToken or fbId is not present')
71 | }
72 | await this.tinder.authorize({
73 | fbToken: this.fb.token,
74 | fbId: this.fb.id
75 | })
76 | return success
77 | } catch (err) {}
78 |
79 | try {
80 | await this.fb.login(silent)
81 | // fb = (await this.mutate(loginFB, {
82 | // silent
83 | // })).loginFB
84 |
85 | await this.tinder.authorize(
86 | { fbToken: this.fb.token, fbId: this.fb.id } as {
87 | fbToken: string
88 | fbId: string
89 | }
90 | )
91 | return success
92 | } catch (err) {
93 | return { status: 'Unauthorized' }
94 | }
95 | }
96 |
97 | public checkDoMatchesExist = async (): Promise => {
98 | const matchesCount = this.state.matches.size
99 | console.log('checkDoMatchesExist', matchesCount)
100 |
101 | if (matchesCount !== 0) {
102 | return true
103 | } else {
104 | const history: any = await new Promise(async resolve => {
105 | let resolved = false
106 | let history: any | null = null
107 |
108 | while (!resolved || history === null) {
109 | try {
110 | history = await this.tinder.getHistory()
111 | resolved = true
112 | } catch (err) {
113 | await this.relogin()
114 | }
115 | }
116 |
117 | resolve(history)
118 | })
119 |
120 | if (history.matches.length === 0) {
121 | return false
122 | } else {
123 | if (history.matches.length !== 0) {
124 | this.state.mergeUpdates(history, true)
125 | return true
126 | } else {
127 | return false
128 | }
129 | }
130 | }
131 | }
132 |
133 | private getUpdates = async () => {
134 | if (this.tinder.subscriptionPromise === null) {
135 | this.tinder.subscriptionPromise = new Promise(async resolve => {
136 | let resolved = false
137 | let updates: any | null = null
138 |
139 | while (!resolved || updates === null) {
140 | try {
141 | updates = await this.tinder.getUpdates()
142 | resolved = true
143 | } catch (err) {
144 | await this.relogin()
145 | }
146 | }
147 |
148 | resolve(updates)
149 | })
150 |
151 | const updates = await this.tinder.subscriptionPromise
152 | this.tinder.subscriptionPromise = null
153 | this.state.mergeUpdates(updates, false)
154 | }
155 | }
156 |
157 | public subscribeToUpdates = async (): Promise => {
158 | if (this.tinder.subscriptionInterval !== null) {
159 | clearInterval(this.tinder.subscriptionInterval)
160 | this.tinder.subscriptionInterval = null
161 | }
162 |
163 | if (this.state.defaults === null) {
164 | while (this.tinder.getDefaults() === null) {
165 | await this.relogin()
166 | }
167 | this.state.setDefaults(this.tinder.getDefaults())
168 | }
169 | const { defaults } = this.state
170 |
171 | const interval = defaults!.globals.updates_interval
172 | this.tinder.subscriptionInterval = window.setInterval(
173 | () => this.getUpdates(),
174 | interval
175 | )
176 |
177 | return success
178 | }
179 |
180 | public logout = () => {
181 | ipcRenderer.send(IPC_LOGOUT)
182 | // const res = await this.mutate(logoutMutation)
183 | // return res.logout
184 | }
185 |
186 | // public getFB = () => {
187 | // return this.query(getFBQuery)
188 | // }
189 |
190 | public getInitialRoute = async (): Promise => {
191 | const matchesCount = this.state.matches.size
192 | console.log({ matchesCount })
193 | if (matchesCount !== 0) {
194 | return routes[VIEW_MATCHES]
195 | } else {
196 | const { token, id } = this.fb
197 | console.log({ token, id })
198 | if (token !== undefined && id !== undefined) {
199 | return routes[VIEW_MATCHES]
200 | } else {
201 | return routes[VIEW_AUTH]
202 | }
203 | }
204 | }
205 |
206 | public showWindow = () => {
207 | ipcRenderer.send(IPC_SHOW_WINDOW)
208 | // return this.mutate(showWindow)
209 | }
210 |
211 | public relogin = async () => {
212 | if (this.reloginPromise === null) {
213 | this.reloginPromise = new Promise(async resolve => {
214 | let loggedIn = false
215 |
216 | while (!loggedIn) {
217 | const online = await isOnline()
218 | if (online) {
219 | const res = await this.login(true)
220 | if (res.status === success.status) {
221 | loggedIn = true
222 | this.reloginPromise!.then(() => {
223 | this.reloginPromise = null
224 | })
225 | } else {
226 | await new Promise(ok => setTimeout(ok, 5000))
227 | }
228 | }
229 | }
230 |
231 | resolve()
232 | })
233 | }
234 |
235 | return this.reloginPromise
236 | }
237 |
238 | public updateProfile = async () => {
239 | const profile = await this.tinder.getProfile()
240 | this.state.defaults!.user.update(profile)
241 | }
242 |
243 | public updatePerson = async (person: PersonType) => {
244 | const newPerson = await this.tinder.getPerson(person._id)
245 | person.update(newPerson)
246 | }
247 |
248 | public sendMessage = async ({ message, matchId }: IAPISendMessage) => {
249 | const match = this.state.matches.get(matchId)!
250 | const rawMessage = {
251 | _id: uuid.v1(),
252 | from: this.state.defaults!.user._id,
253 | to: matchId,
254 | message,
255 | status: PENDING
256 | }
257 | const newMessage = match.addMessage(rawMessage, match.lastMessage)
258 | this.state.addMessageToPending(newMessage)
259 | try {
260 | await this.tinder.sendMessage(matchId, message)
261 | newMessage.changeStatus(SUCCESS)
262 | this.state.addMessageToSent(newMessage)
263 | } catch (err) {
264 | newMessage.changeStatus(FAILURE)
265 | }
266 |
267 | this.state.addMessageToPending(newMessage)
268 | }
269 |
270 | public resendMessage = async (messageId: string) => {
271 | const pendingMessage = this.state.pendingMessages.get(
272 | messageId
273 | ) as MessageType
274 |
275 | pendingMessage.changeStatus(PENDING)
276 | try {
277 | await this.tinder.sendMessage(
278 | pendingMessage.to,
279 | pendingMessage.message
280 | )
281 | pendingMessage.changeStatus(SUCCESS)
282 | this.state.addMessageToSent(pendingMessage)
283 | } catch (err) {
284 | pendingMessage.changeStatus(FAILURE)
285 | }
286 | }
287 | }
288 |
--------------------------------------------------------------------------------
/src/app/stores/API/getFB.graphql:
--------------------------------------------------------------------------------
1 | query getFB {
2 | fb {
3 | id
4 | token
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/stores/API/index.ts:
--------------------------------------------------------------------------------
1 | export { API } from './API'
2 |
--------------------------------------------------------------------------------
/src/app/stores/API/loginFB.graphql:
--------------------------------------------------------------------------------
1 | mutation loginFB {
2 | loginFB {
3 | id
4 | token
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/stores/API/logout.graphql:
--------------------------------------------------------------------------------
1 | mutation logout {
2 | logout {
3 | status
4 | }
5 | }
--------------------------------------------------------------------------------
/src/app/stores/API/showWindow.graphql:
--------------------------------------------------------------------------------
1 | mutation showWindow {
2 | showWindow {
3 | status
4 | }
5 | }
--------------------------------------------------------------------------------
/src/app/stores/API/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { isOnline } from './isOnline'
2 |
--------------------------------------------------------------------------------
/src/app/stores/API/utils/isOnline.ts:
--------------------------------------------------------------------------------
1 | const isReachable = require('is-reachable')
2 | import TinderClient from 'tinder-modern'
3 |
4 | export async function isOnline(): Promise {
5 | const [fb, tinder] = await Promise.all([
6 | isReachable('https://www.facebook.com/'),
7 | TinderClient.isOnline()
8 | ])
9 | return fb && tinder
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/stores/Caches/Caches.ts:
--------------------------------------------------------------------------------
1 | import { CellMeasurerCache } from 'react-virtualized'
2 | import { observable, action } from 'mobx'
3 |
4 | export class Caches {
5 | _messages = new Map()
6 | _ids = new Map()
7 | @observable _gifs = new Map()
8 |
9 | generateKey(id: string, width: number) {
10 | return `${id}_${width}`
11 | }
12 |
13 | getMessagesCache = (id: string, width: number) => {
14 | const key = this.generateKey(id, width)
15 |
16 | if (this._messages.has(key)) {
17 | return this._messages.get(key)
18 | } else {
19 | const cache = new CellMeasurerCache({
20 | fixedWidth: true,
21 | defaultHeight: 33,
22 | defaultWidth: width
23 | })
24 | if (width !== 0) {
25 | this._messages.set(key, cache)
26 | }
27 | return cache
28 | }
29 | }
30 |
31 | getShouldMeasureEverything = (id: string, width: number) => {
32 | const key = this.generateKey(id, width)
33 |
34 | return this._ids.has(key)
35 | }
36 |
37 | forbidMeasureEverything = (id: string, width: number) => {
38 | const key = this.generateKey(id, width)
39 |
40 | this._ids.set(key, true)
41 | }
42 |
43 | @action
44 | setGifStatus = (key: string, status: string) => {
45 | this._gifs.set(key, status)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/app/stores/Caches/index.ts:
--------------------------------------------------------------------------------
1 | export { Caches } from './Caches'
2 |
--------------------------------------------------------------------------------
/src/app/stores/FB/FB.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AbstractFB,
3 | AbstractFBParams,
4 | AbstractFBSaved,
5 | IGetFBTokenFailure,
6 | IGetFBTokenSuccess,
7 | GetFBTokenType
8 | } from '~/shared/definitions'
9 | import fetch from 'node-fetch'
10 | import { ipcRenderer } from 'electron'
11 | import { IPC_GET_FB_TOKEN_REQ, IPC_GET_FB_TOKEN_RES } from '~/shared/constants'
12 | // import getIdFactory from './getIdFactory'
13 | // import getToken from './getToken'
14 | // import loginForceFactory from './loginForceFactory'
15 | // import loginFactory from './loginFactory'
16 | // import { fromCallback } from '~/shared/utils'
17 | // import * as fs from 'fs'
18 |
19 | export class FB extends AbstractFB implements AbstractFB {
20 | constructor(params: AbstractFBParams) {
21 | super()
22 | Object.assign(this, params)
23 | }
24 |
25 | save = () => {
26 | const data: AbstractFBSaved = {
27 | token: this.token,
28 | expiresAt: this.expiresAt,
29 | id: this.id
30 | }
31 |
32 | return this.storage.save('fb', data)
33 | // return fromCallback(callback =>
34 | // fs.writeFile(this.fbPath, JSON.stringify(data), callback)
35 | // )
36 | }
37 |
38 | clear = () => {
39 | return this.storage.save('fb', {})
40 | // return fromCallback(callback => fs.unlink(this.fbPath, callback))
41 | }
42 |
43 | setToken = (token: string) => {
44 | this.token = token
45 | return this.save()
46 | }
47 |
48 | setExpiration = (expiresAt: number) => {
49 | this.expiresAt = expiresAt
50 | return this.save()
51 | }
52 |
53 | setId = (id: string) => {
54 | this.id = id
55 | return this.save()
56 | }
57 |
58 | getId = async () => {
59 | if (typeof this.token === 'undefined') {
60 | throw new Error('fb token is not present!')
61 | }
62 |
63 | if (this.expiresAt === undefined || this.expiresAt <= Date.now()) {
64 | throw new Error('fb token has expired!')
65 | }
66 |
67 | const res = await fetch(
68 | `https://graph.facebook.com/me?fields=id&access_token=${this.token}`
69 | )
70 | const json = await res.json()
71 | if (json.error) {
72 | throw new Error(json.error)
73 | }
74 | if (!res.ok) {
75 | throw new Error(`request failed with status ${res.status}`)
76 | }
77 |
78 | return json.id as string
79 | }
80 | // getId = getIdFactory(this)
81 | // getToken = getToken
82 | getToken = (silent: boolean) => {
83 | const promise = new Promise((resolve, reject) => {
84 | ipcRenderer.once(
85 | IPC_GET_FB_TOKEN_RES,
86 | (_event: Electron.IpcMessageEvent, res: GetFBTokenType) => {
87 | if ((res as IGetFBTokenFailure).err) {
88 | reject((res as IGetFBTokenFailure).err)
89 | } else {
90 | resolve(res as IGetFBTokenSuccess)
91 | }
92 | }
93 | )
94 | })
95 | ipcRenderer.send(IPC_GET_FB_TOKEN_REQ, silent)
96 | return promise
97 | }
98 |
99 | // loginForce = loginForceFactory(this)
100 | loginForce = async (silent: boolean) => {
101 | const { token, expiresIn } = await this.getToken(silent)
102 | this.setToken(token)
103 | this.setExpiration(Date.now() + 1000 * expiresIn)
104 | const id = await this.getId()
105 | this.setId(id)
106 | }
107 |
108 | // login = loginFactory(this)
109 | login = async (silent: boolean) => {
110 | try {
111 | await this.getId()
112 | } catch (err) {
113 | return this.loginForce(silent)
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/app/stores/FB/index.ts:
--------------------------------------------------------------------------------
1 | export { FB } from './FB'
2 |
--------------------------------------------------------------------------------
/src/app/stores/Navigator/Navigator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | VIEW_AUTH,
3 | VIEW_MATCHES,
4 | VIEW_CHAT,
5 | VIEW_USER,
6 | VIEW_LOADING,
7 | VIEW_PROFILE
8 | } from '~/shared/constants'
9 | import { nameToPath } from '~/shared/utils'
10 | import { AbstractAPI } from '~/shared/definitions'
11 | import { History as CustomHistory } from 'history'
12 |
13 | export interface IGQLResponce {
14 | initialRoute: string
15 | }
16 |
17 | export class Navigator {
18 | history: CustomHistory
19 |
20 | setHistory(history: CustomHistory) {
21 | this.history = history
22 | }
23 |
24 | start = async ({ api }: { api: AbstractAPI }) => {
25 | const initialRoute = await api.getInitialRoute()
26 | history.replaceState(
27 | {},
28 | 'Chatinder',
29 | `${location.pathname}#${initialRoute}`
30 | )
31 | await api.showWindow()
32 | }
33 |
34 | push(node: string, params?: string) {
35 | const hash = nameToPath(node, params)
36 | if (`#${hash}` !== location.hash) {
37 | this.history.push(hash)
38 | }
39 | }
40 |
41 | goToAuth() {
42 | this.push(VIEW_AUTH)
43 | }
44 |
45 | goToLoading(title: string) {
46 | this.push(VIEW_LOADING, title)
47 | }
48 |
49 | goToMatches() {
50 | this.push(VIEW_MATCHES)
51 | }
52 |
53 | goToChat(id: string) {
54 | this.push(VIEW_CHAT, id)
55 | }
56 |
57 | goToUser(id: string) {
58 | this.push(VIEW_USER, id)
59 | }
60 |
61 | goToProfile() {
62 | this.push(VIEW_PROFILE)
63 | }
64 |
65 | goBack() {
66 | this.history.goBack()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/app/stores/Navigator/index.ts:
--------------------------------------------------------------------------------
1 | export { Navigator } from './Navigator'
2 |
--------------------------------------------------------------------------------
/src/app/stores/Notifier/Notifier.ts:
--------------------------------------------------------------------------------
1 | import { NotificationMessageType } from '~/shared/definitions'
2 |
3 | export class Notifier {
4 | notify({ title, body }: NotificationMessageType) {
5 | const notification = new Notification(title, {
6 | body
7 | })
8 | setTimeout(notification.close.bind(notification), 5000)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/stores/Notifier/index.ts:
--------------------------------------------------------------------------------
1 | export { Notifier } from './Notifier'
2 |
--------------------------------------------------------------------------------
/src/app/stores/State/Connection.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 |
3 | export const Connection = types.model('Connection', {
4 | id: types.string,
5 | name: types.string,
6 | photo: types.model('ConnectionPhoto', {
7 | small: types.string
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/src/app/stores/State/Defaults.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 | import { Person, Globals } from '.'
3 |
4 | export const Defaults = types.model('Defaults', {
5 | token: types.string,
6 | user: Person,
7 | globals: Globals
8 | })
9 |
--------------------------------------------------------------------------------
/src/app/stores/State/Globals.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 |
3 | export const Globals = types.model('Globals', {
4 | updates_interval: types.number
5 | })
6 |
--------------------------------------------------------------------------------
/src/app/stores/State/Interest.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 |
3 | export const Interest = types.model('Interest', {
4 | name: types.string,
5 | id: types.string
6 | })
7 |
--------------------------------------------------------------------------------
/src/app/stores/State/Job.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 |
3 | export const Job = types.model('Job', {
4 | company: types.maybe(
5 | types.model('JobCompany', {
6 | name: types.string
7 | })
8 | ),
9 | title: types.maybe(
10 | types.model('JobTitle', {
11 | name: types.string
12 | })
13 | )
14 | })
15 |
--------------------------------------------------------------------------------
/src/app/stores/State/Match.ts:
--------------------------------------------------------------------------------
1 | import { types, getEnv } from 'mobx-state-tree'
2 | import { Message, Person } from '.'
3 | import { MessageType } from '~/shared/definitions'
4 |
5 | type LastMessageType = MessageType | null
6 |
7 | interface IRawMatch {
8 | last_activity_date: string
9 | messages: any[]
10 | }
11 |
12 | export const Match = types.model(
13 | 'Match',
14 | {
15 | _id: types.identifier(types.string),
16 | last_activity_date: types.string,
17 | messages: types.array(Message),
18 | is_super_like: types.maybe(types.boolean),
19 | person: Person,
20 |
21 | get lastActivityDate(): string {
22 | return this.last_activity_date
23 | },
24 | get lastMessage(): LastMessageType {
25 | if (this.messages.length === 0) {
26 | return null
27 | } else {
28 | const message = this.messages[this.messages.length - 1]
29 | return message as MessageType
30 | }
31 | }
32 | },
33 | {
34 | addMessage(
35 | message: any,
36 | previousMessage: LastMessageType
37 | ): MessageType {
38 | let newMessage: MessageType
39 | if (previousMessage === null) {
40 | newMessage = Message.create(message)
41 | } else {
42 | newMessage = Message.create({
43 | ...message,
44 | previous: previousMessage._id
45 | })
46 | previousMessage.setNext(newMessage)
47 | }
48 | this.messages.push(newMessage)
49 | return newMessage
50 | },
51 | update(match: IRawMatch) {
52 | this.last_activity_date = match.last_activity_date
53 | match.messages.forEach(message => {
54 | const newMessage = this.addMessage(message, this.lastMessage)
55 | if (newMessage.from === this.person._id) {
56 | getEnv(this).notifier.notify({
57 | title: this.person.name,
58 | body: newMessage.message
59 | })
60 | }
61 | })
62 | }
63 | }
64 | )
65 |
--------------------------------------------------------------------------------
/src/app/stores/State/Message.ts:
--------------------------------------------------------------------------------
1 | import { types, destroy } from 'mobx-state-tree'
2 | import { SUCCESS } from '~/shared/constants'
3 | import { isGIPHY, emojify } from '.'
4 | import * as format from 'date-fns/format'
5 |
6 | // Have to do it manually because TS is not smart enough for recursive types
7 | export interface IMessage {
8 | _id: string
9 | from: string
10 | to: string
11 | sent_date: string
12 | message: string
13 | status: string
14 | previous: IMessage | null
15 | next: IMessage | null
16 |
17 | isGIPHY: boolean
18 | formattedMessage: string
19 | sentDay: string
20 | sentTime: string
21 | sentDate: string
22 | first: boolean
23 | firstInNewDay: boolean
24 |
25 | changeStatus(status: string): void
26 | setPrevious(previous: IMessage | null): void
27 | setNext(next: IMessage | null): void
28 | destroy(): void
29 | }
30 |
31 | export const Message: any = types.model(
32 | 'Message',
33 | {
34 | _id: types.identifier(types.string),
35 | from: types.string,
36 | to: types.string,
37 | sent_date: types.optional(types.string, () => new Date().toISOString()),
38 | message: types.string,
39 | status: types.optional(types.string, SUCCESS),
40 | previous: types.maybe(
41 | types.reference(types.late(() => Message))
42 | ),
43 | next: types.maybe(
44 | types.reference(types.late(() => Message))
45 | ),
46 |
47 | get isGIPHY() {
48 | return isGIPHY(this.message)
49 | },
50 | get formattedMessage() {
51 | if (!this.isGIPHY) {
52 | return emojify(this.message)
53 | } else {
54 | return this.message
55 | }
56 | },
57 | get sentDay() {
58 | return format(this.sent_date, 'MMMM D')
59 | },
60 | get sentTime() {
61 | return format(this.sent_date, 'H:mm')
62 | },
63 | get sentDate() {
64 | return this.sent_date
65 | },
66 | get first() {
67 | if (this.firstInNewDay) {
68 | return true
69 | } else {
70 | if (this.previous!.from !== this.from) {
71 | return true
72 | } else {
73 | return false
74 | }
75 | }
76 | },
77 | get firstInNewDay() {
78 | if (this.previous === null) {
79 | return true
80 | } else {
81 | if (this.previous.sentDay !== this.sentDay) {
82 | return true
83 | } else {
84 | return false
85 | }
86 | }
87 | }
88 | },
89 | {
90 | changeStatus(status: string) {
91 | this.status = status
92 | },
93 | setPrevious(previous: IMessage) {
94 | this.previous = previous
95 | },
96 | setNext(next: IMessage | null) {
97 | this.next = next
98 | },
99 | destroy() {
100 | if (this.previous !== null) {
101 | this.previous.setNext(this.next)
102 | }
103 | if (this.next !== null) {
104 | this.next.setPrevious(this.previous)
105 | }
106 | destroy(this)
107 | }
108 | }
109 | )
110 |
--------------------------------------------------------------------------------
/src/app/stores/State/Person.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 | import { Photo, Job, School, Interest, Connection, emojify } from '.'
3 | import { PhotoType } from '~/shared/definitions'
4 |
5 | interface IGalleryPhoto {
6 | original: string
7 | }
8 |
9 | export const Person = types.model(
10 | 'Person',
11 | {
12 | _id: types.identifier(types.string),
13 | birth_date: types.string,
14 | name: types.string,
15 | photos: types.array(Photo),
16 | bio: types.maybe(types.string),
17 | jobs: types.maybe(types.array(Job)),
18 | schools: types.maybe(types.array(School)),
19 | distance_mi: types.maybe(types.number),
20 | common_interests: types.maybe(types.array(Interest)),
21 | common_connections: types.maybe(types.array(Connection)),
22 | connection_count: types.maybe(types.number),
23 |
24 | get formattedName() {
25 | return emojify(this.name)
26 | },
27 | get formattedBio() {
28 | if (this.bio === null) {
29 | return null
30 | } else {
31 | return emojify(this.bio)
32 | }
33 | },
34 | get smallPhoto(): string {
35 | return this.photos[0].processedFiles[3].url
36 | },
37 | get galleryPhotos(): IGalleryPhoto[] {
38 | return this.photos.map((photo: PhotoType) => ({
39 | original: photo.processedFiles[0].url
40 | }))
41 | },
42 | get distanceKm() {
43 | if (this.distance_mi !== null) {
44 | return Math.round(1.60934 * this.distance_mi)
45 | } else {
46 | return null
47 | }
48 | }
49 | },
50 | {
51 | update(person: any) {
52 | Object.assign(this, person)
53 | }
54 | }
55 | )
56 |
--------------------------------------------------------------------------------
/src/app/stores/State/Photo.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 | import { ProcessedFile } from '.'
3 |
4 | export const Photo = types.model('Photo', {
5 | id: types.identifier(types.string),
6 | url: types.string,
7 | processedFiles: types.array(ProcessedFile)
8 | })
9 |
--------------------------------------------------------------------------------
/src/app/stores/State/ProcessedFile.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 |
3 | export const ProcessedFile = types.model('ProcessedFile', {
4 | width: types.number,
5 | url: types.string,
6 | height: types.number
7 | })
8 |
--------------------------------------------------------------------------------
/src/app/stores/State/Profile.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 |
3 | export const Profile = types.model('Profile', {})
4 |
--------------------------------------------------------------------------------
/src/app/stores/State/School.ts:
--------------------------------------------------------------------------------
1 | import { types } from 'mobx-state-tree'
2 |
3 | export const School = types.model('School', {
4 | name: types.string,
5 | id: types.maybe(types.string)
6 | })
7 |
--------------------------------------------------------------------------------
/src/app/stores/State/State.ts:
--------------------------------------------------------------------------------
1 | import { types, getEnv, destroy } from 'mobx-state-tree'
2 | import { Match, Defaults, Message } from '.'
3 | import { Notifier } from '../Notifier'
4 | import { MatchType, MessageType } from '~/shared/definitions'
5 | import * as Raven from 'raven-js'
6 | import { FAILURE } from '~/shared/constants'
7 |
8 | interface IMatchRaw {
9 | _id: string
10 | messages: any[]
11 | last_activity_date: string
12 | }
13 |
14 | export interface IUpdate {
15 | matches: IMatchRaw[]
16 | blocks: string[]
17 | }
18 |
19 | interface ISorter {
20 | lastActivityDate: string
21 | }
22 |
23 | function sorter(a: ISorter, b: ISorter) {
24 | return Date.parse(b.lastActivityDate) - Date.parse(a.lastActivityDate)
25 | }
26 |
27 | export const State = types.model(
28 | 'State',
29 | {
30 | matches: types.map(Match),
31 | defaults: types.maybe(Defaults),
32 | get sortedMatches(): MatchType[] {
33 | return [...this.matches.values()].sort(sorter)
34 | },
35 | pendingMessages: types.map(types.reference(Message)),
36 | sentMessages: types.map(types.reference(Message))
37 | },
38 | {
39 | setDefaults(defaults: any) {
40 | this.defaults = Defaults.create(defaults)
41 | },
42 | mergeUpdates(updates: IUpdate, silent: boolean) {
43 | const { notifier } = getEnv(this) as { notifier: Notifier }
44 |
45 | this.sentMessages.values().forEach((sentMessage: MessageType) => {
46 | this.sentMessages.delete(sentMessage._id)
47 | sentMessage.destroy()
48 | })
49 |
50 | updates.matches.forEach(match => {
51 | const oldMatch = this.matches.get(match._id)
52 |
53 | if (!oldMatch) {
54 | const { messages } = match
55 | try {
56 | const newMatch = Match.create({
57 | ...match,
58 | messages: []
59 | })
60 |
61 | messages.forEach(message => {
62 | const newMessage = newMatch.addMessage(
63 | message,
64 | newMatch.lastMessage
65 | )
66 | if (
67 | !silent &&
68 | newMessage.from === newMatch.person._id
69 | ) {
70 | notifier.notify({
71 | title: newMatch.person.name,
72 | body: message.message
73 | })
74 | }
75 | })
76 |
77 | this.matches.set(newMatch._id, newMatch)
78 | if (!silent) {
79 | notifier.notify({
80 | title: newMatch.person.name,
81 | body: 'You have a new match!'
82 | })
83 | }
84 | } catch (err) {
85 | Raven.captureException(err)
86 | }
87 | } else {
88 | oldMatch.update(match)
89 | }
90 | })
91 |
92 | const stop = '\uD83D\uDEAB'
93 |
94 | updates.blocks.forEach(id => {
95 | const blocked = this.matches.get(id)
96 |
97 | if (blocked) {
98 | if (!silent) {
99 | notifier.notify({
100 | title: blocked.person.name,
101 | body: `${stop} BLOCKED ${stop}`
102 | })
103 | }
104 | destroy(blocked)
105 | }
106 | })
107 | },
108 | addMessageToPending(message: MessageType) {
109 | this.pendingMessages.put(message)
110 | },
111 | addMessageToSent(message: MessageType) {
112 | this.pendingMessages.delete(message._id)
113 | this.sentMessages.put(message)
114 | },
115 | markAllPendingAsFailed() {
116 | this.pendingMessages.values().forEach((message: MessageType) => {
117 | message.changeStatus(FAILURE)
118 | })
119 | }
120 | }
121 | )
122 |
--------------------------------------------------------------------------------
/src/app/stores/State/index.ts:
--------------------------------------------------------------------------------
1 | export * from './utils'
2 |
3 | export { Message, IMessage } from './Message'
4 | export { ProcessedFile } from './ProcessedFile'
5 | export { Photo } from './Photo'
6 | export { Job } from './Job'
7 | export { School } from './School'
8 | export { Interest } from './Interest'
9 | export { Connection } from './Connection'
10 | export { Person } from './Person'
11 | export { Globals } from './Globals'
12 | export { Defaults } from './Defaults'
13 | export { Match } from './Match'
14 |
15 | export { State } from './State'
16 |
--------------------------------------------------------------------------------
/src/app/stores/State/utils.ts:
--------------------------------------------------------------------------------
1 | import emojione from '~/app/shims/emojione'
2 | import * as he from 'he'
3 |
4 | export function emojify(text: string) {
5 | return emojione.unicodeToImage(he.escape(text))
6 | }
7 |
8 | export function isGIPHY(message: string) {
9 | return /^https?:\/\/(.*)giphy.com/.test(message)
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/stores/Storage/Storage.ts:
--------------------------------------------------------------------------------
1 | import { AbstractStorage } from '~/shared/definitions'
2 |
3 | export class Storage implements AbstractStorage {
4 | async save(key: string, value: any) {
5 | localStorage.setItem(key, JSON.stringify(value))
6 | }
7 |
8 | async get(key: string) {
9 | const rawData = localStorage.getItem(key)
10 | let data: Object = {}
11 |
12 | if (rawData !== null) {
13 | data = JSON.parse(rawData)
14 | }
15 |
16 | return data
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/stores/Storage/index.ts:
--------------------------------------------------------------------------------
1 | export { Storage } from './Storage'
2 |
--------------------------------------------------------------------------------
/src/app/stores/Time/Time.ts:
--------------------------------------------------------------------------------
1 | import * as utils from 'mobx-utils'
2 | import { computed } from 'mobx'
3 |
4 | export class Time {
5 | @computed
6 | get now() {
7 | return utils.now(5000)
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/stores/Time/index.ts:
--------------------------------------------------------------------------------
1 | export { Time } from './Time'
2 |
--------------------------------------------------------------------------------
/src/app/stores/TinderAPI/TinderAPI.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AbstractTinderAPI,
3 | AbstractTinderAPIParams
4 | } from '~/shared/definitions'
5 | import TinderClient from 'tinder-modern'
6 |
7 | export class TinderAPI extends AbstractTinderAPIParams
8 | implements AbstractTinderAPI {
9 | client: TinderClient
10 | subscriptionInterval: number | null = null
11 | subscriptionPromise: Promise | null = null
12 | authPromise: Promise | null = null
13 | authPromiseExternalResolve: ((arg: true) => void) | null = null
14 |
15 | constructor(params: AbstractTinderAPIParams) {
16 | super()
17 | Object.assign(this, params)
18 | this.resetClient()
19 | }
20 |
21 | resetClient = () => {
22 | this.client = new TinderClient({
23 | lastActivityDate: this.lastActivityDate
24 | })
25 | }
26 |
27 | setLastActivityTimestamp = async (lastActivityDate: Date) => {
28 | this.lastActivityDate = lastActivityDate
29 | await this.storage.save('tinder', {
30 | lastActivityTimestamp: lastActivityDate.getTime()
31 | })
32 | }
33 |
34 | isAuthorized = async () => {
35 | try {
36 | await this.getProfile()
37 | return true
38 | } catch (err) {
39 | return false
40 | }
41 | }
42 |
43 | getDefaults = (): any => {
44 | return this.client.getDefaults()
45 | }
46 |
47 | getPerson = (id: string): Promise => {
48 | return this.client.getUser({ userId: id }).then(res => res.results)
49 | }
50 |
51 | getProfile = (): Promise => {
52 | return this.client.getAccount()
53 | }
54 |
55 | sendMessage = async (id: string, message: string): Promise => {
56 | return this.client.sendMessage({
57 | matchId: id,
58 | message
59 | })
60 | }
61 |
62 | authorize = async ({
63 | fbToken,
64 | fbId
65 | }: {
66 | fbToken: string
67 | fbId: string
68 | }) => {
69 | await this.client.authorize({ fbToken, fbId })
70 | }
71 |
72 | getHistory = (): Promise => {
73 | return this.client.getHistory()
74 | }
75 |
76 | getUpdates = async (): Promise => {
77 | const updates = await this.client.getUpdates()
78 | await this.setLastActivityTimestamp(this.client.lastActivity)
79 |
80 | return updates
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/app/stores/TinderAPI/index.ts:
--------------------------------------------------------------------------------
1 | export { TinderAPI } from './TinderAPI'
2 |
--------------------------------------------------------------------------------
/src/app/stores/configureStores.ts:
--------------------------------------------------------------------------------
1 | import { Navigator } from './Navigator'
2 | import { Time } from './Time'
3 | import { Caches } from './Caches'
4 | import { Notifier } from './Notifier'
5 | import { API } from './API'
6 | import { TinderAPI } from './TinderAPI'
7 | import { Storage } from './Storage'
8 | import { State } from './State'
9 | import { FB } from './FB'
10 | import { MST_SNAPSHOT } from '~/shared/constants'
11 | import { AbstractTinderAPISaved, AbstractFBSaved } from '~/shared/definitions'
12 | import { onSnapshot } from 'mobx-state-tree'
13 |
14 | async function getMSTSnapshot(storage: Storage) {
15 | const snapshot = (await storage.get(MST_SNAPSHOT)) as any
16 | if (snapshot.matches == null) {
17 | snapshot.matches = {}
18 | }
19 | if (snapshot.pendingMessages == null) {
20 | snapshot.pendingMessages = {}
21 | }
22 | if (snapshot.sentMessages == null) {
23 | snapshot.sentMessages = {}
24 | }
25 |
26 | return snapshot
27 | }
28 |
29 | async function getTinderSnapshot(storage: Storage) {
30 | const { lastActivityTimestamp } = (await storage.get(
31 | 'tinder'
32 | )) as AbstractTinderAPISaved
33 | let lastActivityDate: Date
34 | if (lastActivityTimestamp != null) {
35 | lastActivityDate = new Date(lastActivityTimestamp)
36 | } else {
37 | lastActivityDate = new Date()
38 | }
39 |
40 | return { lastActivityDate }
41 | }
42 |
43 | function getFBSnapshot(storage: Storage): Promise {
44 | return storage.get('fb')
45 | }
46 |
47 | export async function configureStores() {
48 | const storage = new Storage()
49 | const time = new Time()
50 | const navigator = new Navigator()
51 | const caches = new Caches()
52 | const notifier = new Notifier()
53 |
54 | const snapshot = await getMSTSnapshot(storage)
55 | const state = State.create(snapshot, { notifier })
56 | state.markAllPendingAsFailed()
57 | onSnapshot(state, snapshot => {
58 | storage.save(MST_SNAPSHOT, snapshot)
59 | })
60 |
61 | const tinderProps = await getTinderSnapshot(storage)
62 | const tinder = new TinderAPI({ storage, ...tinderProps })
63 |
64 | const fbProps = await getFBSnapshot(storage)
65 | const fb = new FB({ storage, ...fbProps })
66 |
67 | const api = new API({ state, tinder, fb })
68 | await navigator.start({ api })
69 |
70 | return { navigator, time, caches, api, state }
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/stores/index.ts:
--------------------------------------------------------------------------------
1 | export { configureStores } from './configureStores'
2 |
--------------------------------------------------------------------------------
/src/client.ts:
--------------------------------------------------------------------------------
1 | import { ravenSetupRenderer } from './app/ravenSetupRenderer'
2 | ravenSetupRenderer()
3 | import './app/index'
4 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Chatinder
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/AppManager.ts:
--------------------------------------------------------------------------------
1 | import { AbstractAppManager } from '~/shared/definitions'
2 | import createWindowFactory from './createWindowFactory'
3 | import installExtensions from './installExtensions'
4 | import logoutFactory from './logoutFactory'
5 | import reloadFactory from './reloadFactory'
6 | import showFactory from './showFactory'
7 | import startFactory from './startFactory'
8 |
9 | export class AppManager extends AbstractAppManager
10 | implements AbstractAppManager {
11 | _window: Electron.BrowserWindow | null = null
12 |
13 | get window() {
14 | return this._window
15 | }
16 |
17 | start = startFactory(this)
18 | reload = reloadFactory(this)
19 | createWindow = createWindowFactory(this)
20 | show = showFactory(this)
21 | logout = logoutFactory(this)
22 | installExtensions = installExtensions
23 |
24 | constructor() {
25 | super()
26 | if (process.platform === 'darwin') {
27 | this.forceQuit = false
28 | } else {
29 | this.forceQuit = true
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/createWindowFactory.ts:
--------------------------------------------------------------------------------
1 | import { AbstractAppManager } from '~/shared/definitions'
2 | import { BrowserWindow } from 'electron'
3 | import { resolveRoot } from '~/shared/utils'
4 |
5 | export default function createWindowFactory(instance: AbstractAppManager) {
6 | return function createWindow() {
7 | instance._window = new BrowserWindow({
8 | show: false,
9 | width: 1024,
10 | height: 728,
11 | minWidth: 660,
12 | minHeight: 340,
13 | webPreferences: {
14 | nodeIntegration: true,
15 | blinkFeatures:
16 | 'CSSScrollSnapPoints,CSSSnapSize,ScrollAnchoring,CSSOMSmoothScroll',
17 | experimentalFeatures: true
18 | },
19 | icon: `${resolveRoot()}/icons/icon.png`
20 | })
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/index.ts:
--------------------------------------------------------------------------------
1 | // @flow
2 | export { AppManager } from './AppManager'
3 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/installExtensions.ts:
--------------------------------------------------------------------------------
1 | export default async function installExtensions() {
2 | const installer = require('electron-devtools-installer')
3 |
4 | const extensions = [
5 | 'REACT_DEVELOPER_TOOLS',
6 | 'jdkknkkbebbapilgoeccciglkfbmbnfm' // apollo-devtools
7 | ]
8 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS
9 | if (forceDownload) {
10 | for (const name of extensions) {
11 | try {
12 | const extension = installer[name] || name
13 | await installer.default(extension, forceDownload)
14 | } catch (e) {}
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/logoutFactory.ts:
--------------------------------------------------------------------------------
1 | import { AbstractAppManager } from '~/shared/definitions'
2 | import { fromCallback } from '~/shared/utils'
3 |
4 | export default function logoutFactory(instance: AbstractAppManager) {
5 | return async function logout() {
6 | if (instance._window !== null) {
7 | const { session } = instance._window.webContents
8 | await fromCallback(callback =>
9 | session.clearCache(() => callback(null))
10 | )
11 |
12 | await fromCallback(callback =>
13 | session.clearStorageData({}, callback)
14 | )
15 |
16 | instance._window.destroy()
17 | instance._window = null
18 | } else {
19 | throw new Error('Window was undefined when trying to log out')
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/reloadFactory.ts:
--------------------------------------------------------------------------------
1 | import { AbstractAppManager } from '~/shared/definitions'
2 | import { resolveRoot } from '~/shared/utils'
3 |
4 | export default function reloadFactory(instance: AbstractAppManager) {
5 | return function reload() {
6 | const url = `file://${resolveRoot()}/dist/index.html`
7 | if (instance.window !== null) {
8 | instance.window.loadURL(url)
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/showFactory.ts:
--------------------------------------------------------------------------------
1 | import { AbstractAppManager } from '~/shared/definitions'
2 |
3 | export default function showFactory(instance: AbstractAppManager) {
4 | return function show() {
5 | if (instance.window !== null) {
6 | instance.window.show()
7 | instance.window.focus()
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/startFactory.ts:
--------------------------------------------------------------------------------
1 | import { AbstractAppManager } from '~/shared/definitions'
2 | import { app } from 'electron'
3 | import { updateApp, buildMenu } from './utils'
4 |
5 | export function onBeforeQuitFactory(instance: AbstractAppManager) {
6 | return function onBeforeQuit() {
7 | instance.forceQuit = true
8 | }
9 | }
10 |
11 | export function onCloseFactory(instance: AbstractAppManager) {
12 | return function onClose(event: Event) {
13 | if (!instance.forceQuit) {
14 | event.preventDefault()
15 | if (instance.window != null) {
16 | instance.window.hide()
17 | }
18 | }
19 | }
20 | }
21 |
22 | export function onActivateFactory(instance: AbstractAppManager) {
23 | return function onActivate() {
24 | if (instance.window != null) {
25 | instance.window.restore()
26 | }
27 | }
28 | }
29 |
30 | export default function startFactory(instance: AbstractAppManager) {
31 | return async function start() {
32 | if (!app.isReady()) {
33 | await new Promise(resolve => app.on('ready', resolve))
34 | }
35 |
36 | if (process.env.NODE_ENV === 'development') {
37 | await instance.installExtensions()
38 | require('electron-debug')({ showDevTools: true })
39 | require('electron-context-menu')()
40 | }
41 |
42 | instance.createWindow()
43 | buildMenu()
44 |
45 | const { platform, env } = process
46 | const isWinOrMac = platform === 'win32' || platform === 'darwin'
47 | if (
48 | isWinOrMac &&
49 | env.NODE_ENV !== 'development' &&
50 | instance.window !== null
51 | ) {
52 | instance.window.webContents.once('did-frame-finish-load', () => {
53 | updateApp(instance)
54 | })
55 | }
56 |
57 | app.on('before-quit', onBeforeQuitFactory(instance))
58 | if (instance.window != null) {
59 | instance.window.on('close', onCloseFactory(instance))
60 | }
61 | app.on('activate', onActivateFactory(instance))
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/utils/buildMenu.ts:
--------------------------------------------------------------------------------
1 | import { app, Menu } from 'electron'
2 | import openAboutWindow from 'about-window'
3 | import { resolveRoot } from '~/shared/utils'
4 |
5 | export function buildMenu() {
6 | const template = [
7 | {
8 | label: app.getName(),
9 | submenu: [
10 | {
11 | label: 'About',
12 | click: () => {
13 | openAboutWindow({
14 | icon_path: `${resolveRoot()}/icons/icon.png`,
15 | package_json_dir: resolveRoot(),
16 | bug_report_url:
17 | 'https://github.com/wasd171/chatinder/issues',
18 | homepage: 'https://github.com/wasd171/chatinder',
19 | use_inner_html: true,
20 | adjust_window_size: true,
21 | copyright: `
22 |
23 | Created by Konstantin Nesterov (wasd171 )
24 |
25 | Application logo by Liubov Ruseeva
26 |
27 | Emoji icons supplied by EmojiOne
28 |
29 | Distributed under MIT license
30 |
31 | `
32 | })
33 | }
34 | },
35 | {
36 | type: 'separator' as 'separator'
37 | },
38 | {
39 | role: 'quit' as 'quit'
40 | }
41 | ]
42 | },
43 | {
44 | label: 'Edit',
45 | submenu: [
46 | {
47 | role: 'undo' as 'undo'
48 | },
49 | {
50 | role: 'redo' as 'redo'
51 | },
52 | {
53 | type: 'separator' as 'separator'
54 | },
55 | {
56 | role: 'cut' as 'cut'
57 | },
58 | {
59 | role: 'copy' as 'copy'
60 | },
61 | {
62 | role: 'paste' as 'paste'
63 | }
64 | ]
65 | }
66 | ]
67 |
68 | Menu.setApplicationMenu(Menu.buildFromTemplate(template))
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/utils/index.ts:
--------------------------------------------------------------------------------
1 | export { updateApp } from './updateApp'
2 | export { buildMenu } from './buildMenu'
3 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/AppManager/utils/updateApp.ts:
--------------------------------------------------------------------------------
1 | import { AbstractAppManager } from '~/shared/definitions'
2 | import * as os from 'os'
3 | import { app, autoUpdater, dialog } from 'electron'
4 |
5 | const version = app.getVersion()
6 | const platform = `${os.platform()}_${os.arch()}`
7 | const updateURL = `https://chatinder.herokuapp.com/update/${platform}/${version}`
8 |
9 | export function updateApp(instance: AbstractAppManager) {
10 | autoUpdater.setFeedURL(updateURL)
11 |
12 | // Ask the user if update is available
13 | autoUpdater.on('update-downloaded', (_event, releaseNotes, releaseName) => {
14 | let message = `${app.getName()} ${releaseName} is now available. It will be installed the next time you restart the application.`
15 | if (releaseNotes) {
16 | const splitNotes = releaseNotes.split(/[^\r]\n/)
17 | message += '\n\nRelease notes:\n'
18 | splitNotes.forEach(notes => {
19 | message += notes + '\n\n'
20 | })
21 | }
22 | // Ask user to update the app
23 | dialog.showMessageBox(
24 | {
25 | type: 'question',
26 | buttons: ['Install and Relaunch', 'Later'],
27 | defaultId: 0,
28 | message: `A new version of ${app.getName()} has been downloaded`,
29 | detail: message
30 | },
31 | response => {
32 | if (response === 0) {
33 | instance.forceQuit = true
34 | setTimeout(() => autoUpdater.quitAndInstall(), 1)
35 | }
36 | }
37 | )
38 | })
39 | // init for updates
40 | autoUpdater.checkForUpdates()
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/ServerAPI.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AbstractServerAPI,
3 | AbstractAppManager,
4 | GetFBTokenType
5 | } from '~/shared/definitions'
6 | import { ipcMain, app } from 'electron'
7 | import {
8 | IPC_GET_FB_TOKEN_REQ,
9 | IPC_GET_FB_TOKEN_RES,
10 | IPC_SHOW_WINDOW,
11 | IPC_LOGOUT
12 | } from '~/shared/constants'
13 | import getToken from './getToken'
14 | import { AppManager } from './AppManager'
15 |
16 | export class ServerAPI implements AbstractServerAPI {
17 | private app: AbstractAppManager = new AppManager()
18 |
19 | public start = async () => {
20 | ipcMain.on(IPC_GET_FB_TOKEN_REQ, this.getFBToken)
21 | ipcMain.on(IPC_SHOW_WINDOW, this.showWindow)
22 | ipcMain.on(IPC_LOGOUT, this.logout)
23 | await this.app.start()
24 | this.app.reload()
25 | }
26 |
27 | private async getFBToken(event: Electron.IpcMessageEvent, silent: boolean) {
28 | let res: GetFBTokenType
29 | try {
30 | res = await getToken(silent)
31 | } catch (err) {
32 | res = { err }
33 | }
34 | event.sender.send(IPC_GET_FB_TOKEN_RES, res)
35 | }
36 |
37 | private showWindow = () => {
38 | this.app.show()
39 | }
40 |
41 | private logout = async () => {
42 | await this.app.logout()
43 | app.relaunch()
44 | app.exit(0)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/getToken.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow } from 'electron'
2 | import { URL } from 'url'
3 | import { FBGetTokenType } from '~/shared/definitions'
4 |
5 | export type WindowType = Electron.BrowserWindow | null
6 |
7 | export default function getToken(silent: boolean): Promise {
8 | return new Promise((resolve, reject) => {
9 | let userAgent =
10 | 'Mozilla/5.0 (Linux; U; en-gb; KFTHWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.16 Safari/535.19'
11 |
12 | let authUrl = ``
13 | authUrl += `https://www.facebook.com/v2.6/dialog/oauth?redirect_uri=fb464891386855067://authorize/&`
14 | authUrl += `state={"challenge":"q1WMwhvSfbWHvd8xz5PT6lk6eoA%3D","com.facebook.sdk_client_state":true,`
15 | authUrl += `"3_method":"sfvc_auth"}&scope=user_birthday,user_photos,user_education_history,email,`
16 | authUrl += `user_relationship_details,user_friends,user_work_history,user_likes&response_type=token,`
17 | authUrl += `signed_request&default_audience=friends&return_scopes=true&auth_type=rerequest&`
18 | authUrl += `client_id=464891386855067&ret=login&sdk=ios`
19 |
20 | let win: WindowType = new BrowserWindow({
21 | width: 640,
22 | height: 640,
23 | show: !silent,
24 | webPreferences: {
25 | nodeIntegration: false
26 | }
27 | })
28 |
29 | if (win != null) {
30 | win.on('closed', () => {
31 | if (!silent) {
32 | reject()
33 | }
34 | win = null
35 | })
36 |
37 | win.webContents.on('will-navigate', (_e, url) => {
38 | const raw = /access_token=([^&]*)/.exec(url) || null
39 | const token = raw && raw.length > 1 ? raw[1] : null
40 | const error = /\?error=(.+)$/.exec(url)
41 |
42 | if (!error) {
43 | if (token) {
44 | const expiresStringRegex = /expires_in=(.*)/.exec(url)
45 | let expiresIn
46 | if (
47 | expiresStringRegex !== null &&
48 | expiresStringRegex.length >= 2
49 | ) {
50 | expiresIn = parseInt(expiresStringRegex[1])
51 | } else {
52 | reject(
53 | new Error(
54 | 'Unable to retrieve expiration date from Facebook'
55 | )
56 | )
57 | return
58 | }
59 | // Way to handle Electron bug https://github.com/electron/electron/issues/4374
60 | setImmediate(() => {
61 | if (win !== null) {
62 | win.close()
63 | }
64 | })
65 | resolve({ token, expiresIn })
66 | }
67 | } else {
68 | reject(error)
69 | }
70 | })
71 |
72 | win.webContents.on('did-finish-load', async () => {
73 | if (win !== null) {
74 | let form, action
75 |
76 | const script = `document.getElementById('platformDialogForm')`
77 | form = await asyncExecute(win, script)
78 | if (typeof form !== 'undefined') {
79 | action = await asyncExecute(win, `${script}.action`)
80 | try {
81 | const url = new URL(action)
82 | action = `${url.origin}${url.pathname}`
83 | } catch (err) {
84 | action = null
85 | }
86 | }
87 |
88 | if (
89 | action ===
90 | 'https://m.facebook.com/v2.6/dialog/oauth/confirm'
91 | ) {
92 | asyncExecute(win, `${script}.submit()`)
93 | } else {
94 | if (silent) {
95 | reject()
96 | win = null
97 | }
98 | }
99 | }
100 | })
101 |
102 | win.webContents.on('did-fail-load', () => {
103 | if (silent) {
104 | reject()
105 | win = null
106 | }
107 |
108 | if (win !== null) {
109 | win.loadURL(authUrl, { userAgent: userAgent })
110 | }
111 | })
112 |
113 | win.loadURL(authUrl, { userAgent: userAgent })
114 | }
115 | })
116 | }
117 |
118 | export function asyncExecute(win: WindowType, script: string) {
119 | if (win !== null) {
120 | return win.webContents.executeJavaScript(script, false)
121 | } else {
122 | return Promise.resolve()
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/main/ServerAPI/index.ts:
--------------------------------------------------------------------------------
1 | export { ServerAPI } from './ServerAPI'
2 |
--------------------------------------------------------------------------------
/src/main/ravenSetupMain.ts:
--------------------------------------------------------------------------------
1 | import * as Raven from 'raven'
2 |
3 | export function ravenSetupMain() {
4 | Raven.config(
5 | 'https://da10ea27ad724a2bbd826e378e1c389b:00489345c287416fad67161c62002221@sentry.io/183877'
6 | ).install()
7 | }
8 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import { app } from 'electron'
2 | if (require('electron-squirrel-startup')) {
3 | app.quit()
4 | }
5 | import { ravenSetupMain } from './main/ravenSetupMain'
6 | ravenSetupMain()
7 |
8 | import { ServerAPI } from './main/ServerAPI'
9 | const api = new ServerAPI()
10 | api.start()
11 | // async function main() {
12 | // // const params = await ServerAPI.getInitialProps()
13 | // const api = new ServerAPI()
14 | // api.start()
15 | // }
16 |
17 | // main()
18 |
--------------------------------------------------------------------------------
/src/shared/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const GRAPHQL = 'GRAPHQL'
2 | export const GRAPHQL_SUBSCRIPTIONS = 'GRAPHQL_SUBSCRIPTIONS' //TODO: implement proper subscriptions
3 |
4 | export const SUBSCRIPTION_MATCHES_ALL = 'SUBSCRIPTION_MATCHES_ALL'
5 | export const SUBSCRIPTION_MATCH = 'SUBSCRIPTION_MATCH'
6 | export const SUBSCRIPTION_MATCH_BLOCKED = 'SUBSCRIPTION_MATCH_BLOCKED'
7 |
8 | export {
9 | VIEW_MATCHES,
10 | VIEW_CHAT,
11 | VIEW_USER,
12 | VIEW_PROFILE,
13 | VIEW_AUTH,
14 | VIEW_LOADING
15 | } from './view'
16 | export { routes } from './routes'
17 |
18 | export const SUCCESS = 'SUCCESS'
19 | export const PENDING = 'PENDING'
20 | export const FAILURE = 'FAILURE'
21 | export const PSEUDO = 'PSEUDO'
22 |
23 | export const success = {
24 | status: 'OK'
25 | }
26 |
27 | export const NOTIFICATION = 'NOTIFICATION'
28 | export const KEYCODE_ESC = 27
29 |
30 | export const MST_SNAPSHOT = 'MST_SNAPSHOT'
31 | export * from './ipc'
32 |
--------------------------------------------------------------------------------
/src/shared/constants/ipc.ts:
--------------------------------------------------------------------------------
1 | export const IPC_SHOW_WINDOW = 'IPC_SHOW_WINDOW'
2 | export const IPC_LOGOUT = 'IPC_LOGOUT'
3 | export const IPC_GET_FB_TOKEN_REQ = 'IPC_GET_FB_TOKEN_REQ'
4 | export const IPC_GET_FB_TOKEN_RES = 'IPC_GET_FB_TOKEN_RES'
5 |
--------------------------------------------------------------------------------
/src/shared/constants/routes.ts:
--------------------------------------------------------------------------------
1 | import {
2 | VIEW_MATCHES,
3 | VIEW_CHAT,
4 | VIEW_USER,
5 | VIEW_PROFILE,
6 | VIEW_AUTH,
7 | VIEW_LOADING
8 | } from './view'
9 | import { nameToPath } from '~/shared/utils'
10 |
11 | export const routes = [
12 | VIEW_MATCHES,
13 | VIEW_CHAT,
14 | VIEW_USER,
15 | VIEW_PROFILE,
16 | VIEW_AUTH,
17 | VIEW_LOADING
18 | ].reduce((obj, name) => {
19 | obj[name] = nameToPath(name)
20 | return obj
21 | }, {})
22 |
--------------------------------------------------------------------------------
/src/shared/constants/view.ts:
--------------------------------------------------------------------------------
1 | export const VIEW_MATCHES = 'VIEW_MATCHES'
2 | export const VIEW_CHAT = 'VIEW_CHAT'
3 | export const VIEW_USER = 'VIEW_USER'
4 | export const VIEW_PROFILE = 'VIEW_PROFILE'
5 | export const VIEW_AUTH = 'VIEW_AUTH'
6 | export const VIEW_LOADING = 'VIEW_LOADING'
7 |
--------------------------------------------------------------------------------
/src/shared/definitions/AbstractAPI.ts:
--------------------------------------------------------------------------------
1 | // import { ShowWindowMutation } from '~/schema'
2 | import { PersonType } from '.'
3 |
4 | export interface IAPIGenericReturn {
5 | status: string
6 | }
7 |
8 | export interface IAPISendMessage {
9 | matchId: string
10 | message: string
11 | }
12 |
13 | export abstract class AbstractAPI {
14 | public abstract login(silent: boolean): Promise
15 | public abstract checkDoMatchesExist(): Promise
16 | public abstract subscribeToUpdates(): Promise
17 | public abstract logout(): void
18 | public abstract getInitialRoute(): Promise
19 | public abstract showWindow(): void
20 | public abstract relogin(): Promise
21 | public abstract updateProfile(): Promise
22 | public abstract updatePerson(person: PersonType): Promise
23 | public abstract sendMessage(props: IAPISendMessage): Promise
24 | public abstract resendMessage(messageId: string): Promise
25 | }
26 |
--------------------------------------------------------------------------------
/src/shared/definitions/AbstractAppManager.ts:
--------------------------------------------------------------------------------
1 | export abstract class AbstractAppManager {
2 | _window: Electron.BrowserWindow | null
3 | forceQuit: boolean
4 | abstract get window(): Electron.BrowserWindow | null
5 |
6 | abstract start: () => Promise
7 | abstract reload: () => void
8 | abstract createWindow: () => void
9 | abstract show: () => void
10 | abstract logout: () => Promise
11 | abstract installExtensions: () => Promise
12 | }
13 |
--------------------------------------------------------------------------------
/src/shared/definitions/AbstractFB.ts:
--------------------------------------------------------------------------------
1 | import { FBGetTokenType, AbstractStorage } from '.'
2 |
3 | export abstract class AbstractFBSaved {
4 | token?: string
5 | expiresAt?: number
6 | id?: string
7 | }
8 |
9 | export abstract class AbstractFBParams extends AbstractFBSaved {
10 | storage: AbstractStorage
11 | }
12 |
13 | export abstract class AbstractFB extends AbstractFBParams {
14 | abstract save: () => Promise<{}>
15 | abstract setToken: (token: string) => Promise<{}>
16 | abstract setExpiration: (expiresAt: number) => Promise<{}>
17 | abstract setId: (id: string) => Promise<{}>
18 |
19 | abstract getId: () => Promise
20 | abstract getToken: (silent: boolean) => Promise
21 | abstract loginForce: (silent: boolean) => Promise
22 | abstract login: (silent: boolean) => Promise
23 | abstract clear: () => Promise<{}>
24 | }
25 |
--------------------------------------------------------------------------------
/src/shared/definitions/AbstractServerAPI.ts:
--------------------------------------------------------------------------------
1 | export interface IGetFBTokenFailure {
2 | err: Error
3 | }
4 |
5 | export interface IGetFBTokenSuccess {
6 | token: string
7 | expiresIn: number
8 | }
9 |
10 | export type GetFBTokenType = IGetFBTokenFailure | IGetFBTokenSuccess
11 |
12 | export abstract class AbstractServerAPI {
13 | abstract start: () => Promise
14 | }
15 |
--------------------------------------------------------------------------------
/src/shared/definitions/AbstractStorage.ts:
--------------------------------------------------------------------------------
1 | export abstract class AbstractStorage {
2 | abstract save(key: string, value: any): Promise
3 | abstract get(key: string): Promise
4 | }
5 |
--------------------------------------------------------------------------------
/src/shared/definitions/AbstractTinderAPI.ts:
--------------------------------------------------------------------------------
1 | import { AbstractStorage } from '.'
2 | import TinderClient from 'tinder-modern'
3 |
4 | export abstract class AbstractTinderAPISaved {
5 | lastActivityTimestamp?: number
6 | }
7 |
8 | export abstract class AbstractTinderAPIParams {
9 | lastActivityDate: Date
10 | storage: AbstractStorage
11 | }
12 |
13 | export abstract class AbstractTinderAPI extends AbstractTinderAPIParams {
14 | client: TinderClient
15 | subscriptionInterval: number | null
16 | subscriptionPromise: Promise | null
17 | authPromise: Promise | null
18 | authPromiseExternalResolve: ((arg: true) => void) | null
19 |
20 | abstract resetClient(): void
21 | abstract setLastActivityTimestamp(lastActivityDate: Date): Promise
22 | abstract isAuthorized: () => Promise
23 | abstract getDefaults: () => any
24 | abstract getPerson: (id: string) => Promise
25 | abstract getProfile: () => Promise
26 | abstract sendMessage: (id: string, message: string) => Promise
27 | abstract authorize: (
28 | { fbToken, fbId }: { fbToken: string; fbId: string }
29 | ) => Promise
30 | abstract getHistory: () => Promise
31 | abstract getUpdates: () => Promise
32 | }
33 |
--------------------------------------------------------------------------------
/src/shared/definitions/FBGetTokenType.ts:
--------------------------------------------------------------------------------
1 | export type FBGetTokenType = {
2 | token: string
3 | expiresIn: number
4 | }
5 |
--------------------------------------------------------------------------------
/src/shared/definitions/IGraphQLElectronMessage.ts:
--------------------------------------------------------------------------------
1 | import { PrintedRequest } from 'apollo-client/transport/networkInterface'
2 |
3 | export interface IGraphQLElectronMessage {
4 | id: string
5 | payload: PrintedRequest
6 | }
7 |
--------------------------------------------------------------------------------
/src/shared/definitions/NotificationMessageType.ts:
--------------------------------------------------------------------------------
1 | export type NotificationMessageType = {
2 | title: string
3 | body: string
4 | }
5 |
--------------------------------------------------------------------------------
/src/shared/definitions/State.ts:
--------------------------------------------------------------------------------
1 | import {
2 | State,
3 | Match,
4 | IMessage,
5 | Photo,
6 | Interest,
7 | Person
8 | } from '~/app/stores/State'
9 |
10 | export type StateType = typeof State.Type
11 | export type MatchType = typeof Match.Type
12 | export type MessageType = IMessage
13 | export type PhotoType = typeof Photo.Type
14 | export type InterestType = typeof Interest.Type
15 | export type PersonType = typeof Person.Type
16 |
--------------------------------------------------------------------------------
/src/shared/definitions/index.ts:
--------------------------------------------------------------------------------
1 | export { NotificationMessageType } from './NotificationMessageType'
2 | export { FBGetTokenType } from './FBGetTokenType'
3 |
4 | export { IGraphQLElectronMessage } from './IGraphQLElectronMessage'
5 |
6 | export * from './AbstractServerAPI'
7 | export * from './AbstractAppManager'
8 | export * from './AbstractFB'
9 | export * from './AbstractTinderAPI'
10 | export * from './AbstractAPI'
11 | export * from './AbstractStorage'
12 | export * from './State'
13 |
--------------------------------------------------------------------------------
/src/shared/utils/fromCallback.ts:
--------------------------------------------------------------------------------
1 | type ErrorType = Error | undefined | null
2 | export type IfromCallbackInnerCallback = (err: ErrorType, result?: any) => any
3 | export type IfromCallbackParams = (done: IfromCallbackInnerCallback) => any
4 |
5 | export function fromCallback(func: IfromCallbackParams) {
6 | return new Promise((resolve, reject) => {
7 | func((err, result) => {
8 | if (err != null) {
9 | reject(err)
10 | } else {
11 | resolve(result)
12 | }
13 | })
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/src/shared/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { URL } from 'url'
2 |
3 | export function getNormalizedSizeOfGIPHY(message: string) {
4 | const maxHeight = 170
5 | const maxWidth = 255
6 | const url = new URL(message)
7 |
8 | const strHeight = url.searchParams.get('height')
9 | const strWidth = url.searchParams.get('width')
10 |
11 | if (strHeight === null || strWidth === null) {
12 | throw new Error(
13 | `Unable to get width and/or height for the following giphy: ${message}`
14 | )
15 | }
16 |
17 | let height: number, width: number
18 | height = parseInt(strHeight, 10)
19 | width = parseInt(strWidth, 10)
20 |
21 | if (height > maxHeight) {
22 | width = width * (maxHeight / height)
23 | height = maxHeight
24 | }
25 | if (width > maxWidth) {
26 | height = height * (maxWidth / width)
27 | width = maxWidth
28 | }
29 |
30 | return { height, width }
31 | }
32 |
33 | export { nameToPath } from './nameToPath'
34 | export { resolveRoot } from './resolveRoot'
35 | export { fromCallback } from './fromCallback'
36 |
--------------------------------------------------------------------------------
/src/shared/utils/nameToPath.ts:
--------------------------------------------------------------------------------
1 | import {
2 | VIEW_CHAT,
3 | VIEW_USER,
4 | VIEW_LOADING,
5 | VIEW_MATCHES,
6 | VIEW_PROFILE
7 | } from '~/shared/constants/view'
8 |
9 | export function nameToPath(name: string, param?: string): string {
10 | switch (name) {
11 | case VIEW_CHAT:
12 | return `${nameToPath(VIEW_MATCHES)}/${param || ':id'}/${VIEW_CHAT}`
13 | case VIEW_USER:
14 | return `${nameToPath(VIEW_MATCHES)}/${param || ':id'}/${VIEW_USER}`
15 | case VIEW_LOADING:
16 | return `/${VIEW_LOADING}/${param || ':title'}`
17 | case VIEW_PROFILE:
18 | return `${nameToPath(VIEW_MATCHES)}/${VIEW_PROFILE}`
19 | default:
20 | return `/${name}`
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/shared/utils/resolveRoot.ts:
--------------------------------------------------------------------------------
1 | export function resolveRoot(): string {
2 | const { path } = require('app-root-path')
3 | return path
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "removeComments": false,
5 | "preserveConstEnums": true,
6 | "sourceMap": true,
7 | "declaration": false,
8 | "noImplicitAny": true,
9 | "noImplicitReturns": true,
10 | "suppressImplicitAnyIndexErrors": true,
11 | "strictNullChecks": true,
12 | "noUnusedLocals": true,
13 | "noImplicitThis": true,
14 | "noUnusedParameters": true,
15 | "importHelpers": true,
16 | "noEmitHelpers": true,
17 | "module": "es6",
18 | "moduleResolution": "node",
19 | "pretty": true,
20 | "target": "ES2016",
21 | "jsx": "react",
22 | "experimentalDecorators": true,
23 | "paths": {
24 | "~/*": ["*"]
25 | }
26 | },
27 | "formatCodeOptions": {
28 | "indentSize": 4,
29 | "tabSize": 4
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'is-reachable' {
2 | function isReachable(url: string): Promise
3 | export = isReachable
4 | }
5 |
6 | declare module '*.graphql' {
7 | import { DocumentNode, Location, DefinitionNode } from 'graphql'
8 | export const kind: 'Document'
9 | export const loc: Location | undefined
10 | export const definitions: Array
11 | }
12 |
13 | declare module 'emojione/package.json' {
14 | export const version: string
15 | }
16 |
17 | declare namespace NodeJS {
18 | export interface Global {
19 | jQuery: any
20 | emojione: any
21 | emojioneVersion: string
22 | Perf: any
23 | }
24 | }
25 |
26 | declare module 'react-image-gallery' {
27 | import * as React from 'react'
28 |
29 | export type ClickHandler = React.MouseEventHandler
30 | type CustomRenderer = (
31 | onClick: ClickHandler,
32 | disabled: boolean
33 | ) => JSX.Element
34 |
35 | export interface Item {
36 | original: string
37 | }
38 |
39 | interface Props {
40 | items: Item[]
41 | showPlayButton: boolean
42 | showBullets: boolean
43 | showThumbnails: boolean
44 | showFullscreenButton: boolean
45 | infinite: boolean
46 | renderLeftNav: CustomRenderer
47 | renderRightNav: CustomRenderer
48 | }
49 |
50 | class ImageGallery extends React.Component {}
51 | export default ImageGallery
52 | }
53 |
54 | declare module 'material-ui/TextField/TextFieldHint' {
55 | import * as React from 'react'
56 | import { MuiTheme } from 'material-ui/styles'
57 |
58 | interface Props {
59 | muiTheme: MuiTheme
60 | show: boolean
61 | text: string
62 | }
63 |
64 | class TextFieldHint extends React.Component {}
65 |
66 | export default TextFieldHint
67 | }
68 |
69 | declare module 'material-ui/TextField/TextFieldUnderline' {
70 | import * as React from 'react'
71 | import { MuiTheme } from 'material-ui/styles'
72 |
73 | interface Props {
74 | muiTheme: MuiTheme
75 | disabled: boolean
76 | focus: boolean
77 | }
78 |
79 | class TextFieldUnderline extends React.Component {}
80 |
81 | export default TextFieldUnderline
82 | }
83 |
84 | declare module 'react-content-loader' {
85 | import * as React from 'react'
86 |
87 | interface ILoaderProps {
88 | style?: Object
89 | type?: string
90 | speed?: number
91 | width?: number
92 | height?: number
93 | primaryColor?: string
94 | secondaryColor?: string
95 | }
96 |
97 | export default class ContentLoader extends React.Component {}
98 |
99 | interface ICircleProps {
100 | x: number
101 | y: number
102 | radius: number
103 | }
104 |
105 | export class Circle extends React.Component {}
106 |
107 | interface IRectProps extends ICircleProps {
108 | width: number
109 | height: number
110 | }
111 |
112 | export class Rect extends React.Component {}
113 | }
114 |
115 | declare module 'electron-is-dev' {
116 | const isDev: boolean
117 | export default isDev
118 | }
119 |
120 | declare module 'app-root-path' {
121 | const path: string
122 | export = { path }
123 | }
124 |
--------------------------------------------------------------------------------
/webpack/base.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const path = require('path')
3 | // const { TsConfigPathsPlugin } = require('awesome-typescript-loader')
4 | const BabiliPlugin = require('babili-webpack-plugin')
5 | const isDev = require('./isDev')
6 | const HappyPack = require('happypack')
7 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
8 |
9 | const commonPlugins = [
10 | new webpack.DefinePlugin({
11 | 'process.env.NODE_ENV': JSON.stringify(
12 | process.env.NODE_ENV || 'production'
13 | )
14 | }),
15 | new HappyPack({
16 | id: 'ts',
17 | threads: 2,
18 | loaders: [
19 | {
20 | path: 'ts-loader',
21 | query: { happyPackMode: true }
22 | }
23 | ]
24 | })
25 | ]
26 | const devPlugins = [new ForkTsCheckerWebpackPlugin()]
27 | const productionPlugins = [new BabiliPlugin()]
28 | const plugins = isDev
29 | ? [...commonPlugins, ...devPlugins]
30 | : [...productionPlugins, ...commonPlugins]
31 |
32 | module.exports = {
33 | output: {
34 | path: path.join(__dirname, '..', 'dist')
35 | },
36 |
37 | // Enable sourcemaps for debugging webpack's output.
38 | devtool: isDev ? 'source-map' : undefined,
39 |
40 | resolve: {
41 | // Add '.ts' and '.tsx' as resolvable extensions.
42 | extensions: ['.ts', '.tsx', '.js', '.json'],
43 | // plugins: [new TsConfigPathsPlugin()]
44 | alias: {
45 | '~': path.resolve(__dirname, '..', 'src')
46 | }
47 | },
48 |
49 | module: {
50 | rules: [
51 | // All files with a '.ts' or '.tsx' extension will be handled by 'awesome-typescript-loader'.
52 | {
53 | test: /\.tsx?$/,
54 | loader: 'happypack/loader?id=ts',
55 | exclude: /node_modules/
56 | },
57 |
58 | // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
59 | {
60 | enforce: 'pre',
61 | test: /\.js$/,
62 | loader: 'source-map-loader',
63 | exclude: [
64 | new RegExp(`node_modules\\${path.sep}apollo-client`),
65 | new RegExp(`node_modules\\${path.sep}graphql-tools`),
66 | new RegExp(`node_modules\\${path.sep}deprecated-decorator`)
67 | ]
68 | },
69 |
70 | // Handle .graphql
71 | { test: /\.graphql$/, loader: 'graphql-tag/loader' }
72 | ]
73 | },
74 | plugins: plugins,
75 | watch: isDev,
76 | watchOptions: {
77 | ignored: /node_modules/
78 | },
79 | externals: (context, request, callback) => {
80 | if (/(about-window|app-root-path)/.test(request)) {
81 | callback(null, 'commonjs ' + request)
82 | } else {
83 | callback()
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/webpack/dll.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const base = require('./base')
3 | const merge = require('webpack-merge')
4 | const path = require('path')
5 |
6 | const dll = {
7 | entry: {
8 | vendor: [
9 | 'date-fns',
10 | 'emojione',
11 | 'emojionearea',
12 | 'he',
13 | 'jquery',
14 | 'lodash.trim',
15 | 'material-ui',
16 | 'mobx',
17 | 'mobx-react',
18 | 'mobx-utils',
19 | 'mobx-state-tree',
20 | 'raven-js',
21 | 'react',
22 | 'react-dom',
23 | 'react-image-gallery',
24 | 'react-router',
25 | 'react-router-dom',
26 | 'react-tap-event-plugin',
27 | 'react-virtualized',
28 | 'react-waypoint',
29 | 'simplebar',
30 | 'styled-components',
31 | 'tinder-modern',
32 | 'is-reachable'
33 | ]
34 | },
35 | output: {
36 | filename: '[name].js',
37 | libraryTarget: 'commonjs2'
38 | },
39 | target: 'electron-renderer',
40 | plugins: [
41 | new webpack.DllPlugin({
42 | path: path.join(base.output.path, '[name]-manifest.json')
43 | })
44 | ]
45 | }
46 |
47 | module.exports = merge(base, dll)
48 |
--------------------------------------------------------------------------------
/webpack/isDev.js:
--------------------------------------------------------------------------------
1 | const { NODE_ENV } = process.env
2 | const isDev = NODE_ENV === 'development'
3 | module.exports = isDev
4 |
--------------------------------------------------------------------------------
/webpack/main.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const base = require('./base')
3 | const merge = require('webpack-merge')
4 | const nodeExternals = require('webpack-node-externals')
5 | const { StatsWriterPlugin } = require('webpack-stats-plugin')
6 | const isDev = require('./isDev')
7 | const path = require('path')
8 |
9 | const devPlugins = [
10 | new StatsWriterPlugin({
11 | filename: 'stats-main.json',
12 | fields: null
13 | })
14 | ]
15 |
16 | const plugins = isDev ? devPlugins : []
17 |
18 | const main = {
19 | entry: './src/server.ts',
20 | output: {
21 | filename: 'main.js'
22 | },
23 | target: 'electron',
24 | plugins
25 | }
26 |
27 | module.exports = merge(base, main)
28 |
--------------------------------------------------------------------------------
/webpack/renderer.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 | const path = require('path')
3 | const base = require('./base')
4 | const merge = require('webpack-merge')
5 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
6 | const CopyWebpackPlugin = require('copy-webpack-plugin')
7 | const { StatsWriterPlugin } = require('webpack-stats-plugin')
8 | const isDev = require('./isDev')
9 |
10 | const commonPlugins = [
11 | new webpack.DllReferencePlugin({
12 | context: '.',
13 | manifest: require(path.join(base.output.path, 'vendor-manifest.json')),
14 | sourceType: 'commonjs2',
15 | name: './vendor.js'
16 | }),
17 | new ExtractTextPlugin('styles.css'),
18 | new CopyWebpackPlugin([
19 | {
20 | from: 'src/index.html',
21 | to: 'index.html'
22 | },
23 | {
24 | from: 'node_modules/emojione/assets/png',
25 | to: 'emoji'
26 | }
27 | ])
28 | ]
29 | const plugins = isDev
30 | ? [
31 | ...commonPlugins,
32 | new StatsWriterPlugin({
33 | filename: 'stats-renderer.json',
34 | fields: null
35 | })
36 | ]
37 | : commonPlugins
38 |
39 | const renderer = {
40 | entry: './src/client.ts',
41 | output: {
42 | filename: 'renderer.js'
43 | },
44 | target: 'electron-renderer',
45 | module: {
46 | rules: [
47 | {
48 | test: /\.css$/,
49 | use: ExtractTextPlugin.extract({
50 | use: ['css-loader', 'resolve-url-loader']
51 | })
52 | },
53 | {
54 | test: /\.(woff2)(\?[a-z0-9=&.]+)?$/,
55 | loader: 'file-loader',
56 | options: {
57 | name: 'fonts/[name].[ext]?[hash]'
58 | }
59 | },
60 | {
61 | test: /\.(ttf|eot|svg|woff)(\?[a-z0-9=&.]+)?$/, // Needs to be used with caution
62 | loader: 'skip-loader'
63 | }
64 | ]
65 | },
66 | plugins
67 | }
68 |
69 | module.exports = merge(base, renderer)
70 |
--------------------------------------------------------------------------------