├── .nvmrc ├── app ├── .nvmrc ├── src │ ├── components │ │ ├── Dashboard │ │ │ ├── WarnRefreshModal.tsx │ │ │ ├── Wrapped │ │ │ │ ├── Watermark.tsx │ │ │ │ ├── AnimationRunner.ts │ │ │ │ ├── TimerBar.tsx │ │ │ │ ├── Sections │ │ │ │ │ ├── WrappedLoading.tsx │ │ │ │ │ ├── YouTexting.tsx │ │ │ │ │ ├── ThereWereFunnyMoments.tsx │ │ │ │ │ ├── OtherFriendsToo.tsx │ │ │ │ │ ├── Thanks.tsx │ │ │ │ │ ├── WrappedIntro.tsx │ │ │ │ │ └── WrappedError.tsx │ │ │ │ └── Messages.tsx │ │ │ ├── GlobalContext.tsx │ │ │ └── NotificationSettingsModal.tsx │ │ ├── Premium │ │ │ ├── constants.ts │ │ │ ├── GoldContext.tsx │ │ │ └── UnlockPremiumButton.tsx │ │ ├── Filters │ │ │ ├── LimitFilter.tsx │ │ │ └── GroupChatFilter.tsx │ │ ├── Loaders │ │ │ ├── GraphContainerLoading.tsx │ │ │ ├── BarChartLoading.tsx │ │ │ └── InitializingTextSlider.tsx │ │ ├── LoadingEllipsis.tsx │ │ ├── Home │ │ │ ├── Redirecter.tsx │ │ │ ├── Onboarding.tsx │ │ │ └── Home.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── ComingSoon.tsx │ │ ├── Float.tsx │ │ ├── Footer.tsx │ │ ├── Support │ │ │ └── ErrorPage.tsx │ │ └── messages.module.scss │ ├── constants │ │ ├── index.ts │ │ ├── objReplacementUnicode.ts │ │ ├── versions.ts │ │ ├── api.ts │ │ ├── emojis.ts │ │ ├── types.ts │ │ ├── reactions.ts │ │ ├── punctuation.ts │ │ └── filters.ts │ ├── custom.d.ts │ ├── utils │ │ ├── lowerCaseList.ts │ │ ├── delimList.ts │ │ ├── files.ts │ │ ├── analytics.ts │ │ ├── amplitudeClient.ts │ │ ├── normalization.ts │ │ ├── overTimeHelpers.ts │ │ ├── sqliteWrapper.ts │ │ └── db.ts │ ├── __tests__ │ │ └── App.test.tsx │ ├── renderer │ │ ├── index.ejs │ │ ├── preload.d.ts │ │ └── index.tsx │ ├── main │ │ ├── LiveDb │ │ │ └── getRespondReminders.ts │ │ ├── preload.ts │ │ └── util.ts │ ├── theme │ │ └── index.ts │ ├── analysis │ │ ├── queries │ │ │ ├── InboxWriteQuery.ts │ │ │ ├── EarliestAndLatestDatesQuery.ts │ │ │ ├── filters │ │ │ │ └── sharedGroupChatTabFilters.ts │ │ │ ├── AverageDelayQuery.ts │ │ │ ├── TotalSentimentQuery.ts │ │ │ ├── FriendsOverTimeQuery.ts │ │ │ ├── RawMessageQuery.ts │ │ │ ├── TotalSentVsReceivedQuery.ts │ │ │ ├── GroupChats │ │ │ │ ├── GroupChatActivityOverTimeQuery.ts │ │ │ │ ├── GroupChatByFriendsQuery.ts │ │ │ │ └── GroupChatReactionsQuery.ts │ │ │ ├── TimeOfDayQuery.ts │ │ │ ├── ContactOptionsQuery.ts │ │ │ ├── WrappedQueries │ │ │ │ ├── MostPopularDayQuery.ts │ │ │ │ └── FunniestMessageQuery.ts │ │ │ ├── RespondReminders.ts │ │ │ ├── TextsOverTimeQuery.ts │ │ │ ├── SentimentOverTimeQuery.ts │ │ │ └── TopSentimentFriendsQuery.ts │ │ ├── tables │ │ │ ├── types.ts │ │ │ ├── CalendarTable.ts │ │ │ ├── GroupChatCoreTable.ts │ │ │ ├── ChatTable.ts │ │ │ └── SentimentTable.ts │ │ └── directories.ts │ └── hooks │ │ └── useKeyPress.ts ├── .erb │ ├── mocks │ │ └── fileMock.js │ ├── configs │ │ ├── .eslintrc │ │ ├── webpack.config.eslint.ts │ │ ├── webpack.paths.ts │ │ ├── webpack.config.base.ts │ │ ├── webpack.config.renderer.dev.dll.ts │ │ ├── webpack.config.main.prod.ts │ │ └── webpack.config.preload.dev.ts │ └── scripts │ │ ├── .eslintrc │ │ ├── delete-source-maps.js │ │ ├── link-modules.ts │ │ ├── check-node-env.js │ │ ├── clean.js │ │ ├── check-port-in-use.js │ │ ├── electron-rebuild.js │ │ ├── check-build-exists.ts │ │ ├── notarize.js │ │ └── check-native-dep.js ├── assets │ ├── icon.ico │ ├── icon.png │ ├── icon.icns │ ├── icons │ │ ├── 16x16.png │ │ ├── 32x32.png │ │ ├── 64x64.png │ │ ├── 128x128.png │ │ ├── 256x256.png │ │ ├── 512x512.png │ │ ├── 1024x1024.png │ │ └── tray_icon.png │ ├── entitlements.mac.plist │ └── assets.d.ts ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── .gitattributes ├── .gitignore ├── .eslintignore ├── release │ └── app │ │ └── package.json ├── tsconfig.json └── .eslintrc.js ├── web ├── .env.development ├── src │ ├── __mocks__ │ │ └── fileMock.js │ ├── theme │ │ ├── semantics.ts │ │ ├── colors.ts │ │ └── index.ts │ ├── constants │ │ └── APP_VERSION.ts │ ├── custom.d.ts │ ├── utils │ │ ├── validation.ts │ │ ├── addContact.ts │ │ ├── firestore.ts │ │ ├── validation.test.ts │ │ └── gtag.ts │ ├── hooks │ │ ├── usePrevious.ts │ │ └── useGradient.ts │ ├── components │ │ ├── types.ts │ │ ├── DefaultContentContainer.tsx │ │ ├── HighlightedText.tsx │ │ ├── Input.tsx │ │ ├── Gradient.tsx │ │ ├── Markdown.tsx │ │ ├── charts │ │ │ └── BarChart.tsx │ │ ├── TextNotification.tsx │ │ └── Navbar.tsx │ ├── __tests__ │ │ ├── index.test.tsx │ │ └── landing.test.tsx │ └── pages │ │ ├── index.tsx │ │ └── _document.tsx ├── .vscode │ └── settings.json ├── public │ ├── ICON.png │ ├── adam.png │ ├── nate.png │ ├── allison.png │ ├── annie.png │ ├── cathy.png │ ├── favicon.ico │ ├── george.png │ ├── isabel.png │ ├── jackie.png │ ├── alexander.png │ ├── GitHub_Icon.png │ ├── favicon-114.png │ ├── favicon-120.png │ ├── favicon-144.png │ ├── favicon-150.png │ ├── favicon-152.png │ ├── favicon-16.png │ ├── favicon-160.png │ ├── favicon-180.png │ ├── favicon-192.png │ ├── favicon-310.png │ ├── favicon-32.png │ ├── favicon-57.png │ ├── favicon-60.png │ ├── favicon-64.png │ ├── favicon-70.png │ ├── favicon-72.png │ ├── favicon-76.png │ ├── favicon-96.png │ ├── floating_app.png │ ├── floating_app_five.png │ ├── floating_app_four.png │ ├── floating_app_two.png │ ├── floating_app_three.png │ ├── browserconfig.xml │ └── vercel.svg ├── .env.example ├── next-env.d.ts ├── .babelrc ├── jest.config.js ├── setUpJest.js ├── .eslintrc ├── next.config.js ├── tsconfig.json ├── README.md └── package.json ├── Procfile ├── server ├── .eslintignore ├── .env.sample ├── .eslintrc ├── README.md ├── tsconfig.json ├── src │ ├── router.ts │ ├── controllers │ │ ├── ContactsController.ts │ │ └── HelpController.ts │ └── index.ts └── package.json ├── lerna.json ├── yarn.lock ├── .prettierrc ├── eslint-config ├── yarn.lock ├── package-lock.json ├── README.md ├── package.json └── index.js ├── assets └── documentation │ ├── Header-Graphic.png │ ├── Analytics-Graphic.png │ ├── Supercharge-Graphic.png │ ├── Texts-Over-Time-Graphic.png │ └── blog.md ├── package.json ├── .github ├── ISSUE_TEMPLATE │ ├── contact_us.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── web-test.yml │ ├── app-test.yml │ └── app-publish.yml ├── .vscode └── settings.json ├── .gitignore └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.13.0 2 | -------------------------------------------------------------------------------- /app/.nvmrc: -------------------------------------------------------------------------------- 1 | 15.9.0 2 | -------------------------------------------------------------------------------- /web/.env.development: -------------------------------------------------------------------------------- 1 | ENV=development -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start --prefix server 2 | -------------------------------------------------------------------------------- /server/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | node_modules/* -------------------------------------------------------------------------------- /app/src/components/Dashboard/WarnRefreshModal.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = '' 2 | -------------------------------------------------------------------------------- /server/.env.sample: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | EMAIL= 3 | EMAIL_PASSWORD= -------------------------------------------------------------------------------- /app/.erb/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /app/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_FILTER_LIMIT = 15; 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["app", "web"], 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /app/src/constants/objReplacementUnicode.ts: -------------------------------------------------------------------------------- 1 | export const objReplacementUnicode = 65532; 2 | -------------------------------------------------------------------------------- /app/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icon.ico -------------------------------------------------------------------------------- /app/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icon.png -------------------------------------------------------------------------------- /app/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'emojis-list' { 2 | export const emojis: string[]; 3 | } 4 | -------------------------------------------------------------------------------- /web/public/ICON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/ICON.png -------------------------------------------------------------------------------- /web/public/adam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/adam.png -------------------------------------------------------------------------------- /web/public/nate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/nate.png -------------------------------------------------------------------------------- /app/assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icon.icns -------------------------------------------------------------------------------- /web/public/allison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/allison.png -------------------------------------------------------------------------------- /web/public/annie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/annie.png -------------------------------------------------------------------------------- /web/public/cathy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/cathy.png -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/george.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/george.png -------------------------------------------------------------------------------- /web/public/isabel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/isabel.png -------------------------------------------------------------------------------- /web/public/jackie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/jackie.png -------------------------------------------------------------------------------- /web/public/alexander.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/alexander.png -------------------------------------------------------------------------------- /app/assets/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icons/16x16.png -------------------------------------------------------------------------------- /app/assets/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icons/32x32.png -------------------------------------------------------------------------------- /app/assets/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icons/64x64.png -------------------------------------------------------------------------------- /web/public/GitHub_Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/GitHub_Icon.png -------------------------------------------------------------------------------- /web/public/favicon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-114.png -------------------------------------------------------------------------------- /web/public/favicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-120.png -------------------------------------------------------------------------------- /web/public/favicon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-144.png -------------------------------------------------------------------------------- /web/public/favicon-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-150.png -------------------------------------------------------------------------------- /web/public/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-152.png -------------------------------------------------------------------------------- /web/public/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-16.png -------------------------------------------------------------------------------- /web/public/favicon-160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-160.png -------------------------------------------------------------------------------- /web/public/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-180.png -------------------------------------------------------------------------------- /web/public/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-192.png -------------------------------------------------------------------------------- /web/public/favicon-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-310.png -------------------------------------------------------------------------------- /web/public/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-32.png -------------------------------------------------------------------------------- /web/public/favicon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-57.png -------------------------------------------------------------------------------- /web/public/favicon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-60.png -------------------------------------------------------------------------------- /web/public/favicon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-64.png -------------------------------------------------------------------------------- /web/public/favicon-70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-70.png -------------------------------------------------------------------------------- /web/public/favicon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-72.png -------------------------------------------------------------------------------- /web/public/favicon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-76.png -------------------------------------------------------------------------------- /web/public/favicon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/favicon-96.png -------------------------------------------------------------------------------- /web/public/floating_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/floating_app.png -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icons/128x128.png -------------------------------------------------------------------------------- /app/assets/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icons/256x256.png -------------------------------------------------------------------------------- /app/assets/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icons/512x512.png -------------------------------------------------------------------------------- /app/src/components/Premium/constants.ts: -------------------------------------------------------------------------------- 1 | export const STRIPE_LINK = 'https://buy.stripe.com/aEU186arN4UO6jK6ot'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true, 4 | "semi": false, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /app/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /app/assets/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icons/1024x1024.png -------------------------------------------------------------------------------- /app/assets/icons/tray_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/app/assets/icons/tray_icon.png -------------------------------------------------------------------------------- /web/public/floating_app_five.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/floating_app_five.png -------------------------------------------------------------------------------- /web/public/floating_app_four.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/floating_app_four.png -------------------------------------------------------------------------------- /web/public/floating_app_two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/floating_app_two.png -------------------------------------------------------------------------------- /eslint-config/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_FIREBASE_API_KEY= 2 | NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= 3 | NEXT_PUBLIC_FIREBASE_PROJECT_ID= 4 | -------------------------------------------------------------------------------- /web/public/floating_app_three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/web/public/floating_app_three.png -------------------------------------------------------------------------------- /app/src/constants/versions.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../../release/app/package.json'; 2 | 3 | export const APP_VERSION = version; 4 | -------------------------------------------------------------------------------- /assets/documentation/Header-Graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/assets/documentation/Header-Graphic.png -------------------------------------------------------------------------------- /assets/documentation/Analytics-Graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/assets/documentation/Analytics-Graphic.png -------------------------------------------------------------------------------- /eslint-config/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leftonread/eslint-config", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /assets/documentation/Supercharge-Graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/assets/documentation/Supercharge-Graphic.png -------------------------------------------------------------------------------- /eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # Eslint Config - Left on Read 2 | 3 | A shared eslint config to be used throughout the various projects in this monorepo. -------------------------------------------------------------------------------- /app/src/utils/lowerCaseList.ts: -------------------------------------------------------------------------------- 1 | export function lowerCaseList(myList: string[]): string[] { 2 | return myList.map((t) => t.toLowerCase()); 3 | } 4 | -------------------------------------------------------------------------------- /web/src/theme/semantics.ts: -------------------------------------------------------------------------------- 1 | export const shadow = 'rgba(0, 0, 0, 0.25)' 2 | 3 | export const success = '#4BCA81' 4 | export const error = '#FF9494' 5 | -------------------------------------------------------------------------------- /assets/documentation/Texts-Over-Time-Graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Left-on-Read/leftonread/HEAD/assets/documentation/Texts-Over-Time-Graphic.png -------------------------------------------------------------------------------- /app/src/utils/delimList.ts: -------------------------------------------------------------------------------- 1 | export function delimList(myList: string[]): string { 2 | const l = myList.map((t) => `"${t}"`); 3 | return l.join(', '); 4 | } 5 | -------------------------------------------------------------------------------- /app/.erb/configs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/.erb/configs/webpack.config.eslint.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | 3 | module.exports = require('./webpack.config.renderer.dev').default; 4 | -------------------------------------------------------------------------------- /app/src/constants/api.ts: -------------------------------------------------------------------------------- 1 | export const API_BASE_URL = 2 | process.env.NODE_ENV === 'production' 3 | ? 'https://leftonread.herokuapp.com/api' 4 | : 'http://localhost:8080/api'; 5 | -------------------------------------------------------------------------------- /web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // NOTE: This file should not be edited 4 | // see https://nextjs.org/docs/basic-features/typescript for more information. 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "left-on-read", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "postinstall": "yarn --cwd server --production=false; yarn --cwd server build" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /web/src/constants/APP_VERSION.ts: -------------------------------------------------------------------------------- 1 | /* This file is auto-edited, please do not edit it or move it unless you are touching release pipelines */ 2 | export const LATEST_APP_VERSION_FOR_MARKETING_SITE = '4.0.1' 3 | -------------------------------------------------------------------------------- /app/.erb/scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off", 4 | "global-require": "off", 5 | "import/no-dynamic-require": "off", 6 | "import/no-extraneous-dependencies": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /web/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.svg' { 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | const content: any 6 | export default content 7 | } 8 | -------------------------------------------------------------------------------- /app/src/constants/emojis.ts: -------------------------------------------------------------------------------- 1 | import emojis from 'emojis-list'; 2 | 3 | import { delimList } from '../utils/delimList'; 4 | 5 | export const getEmojiData = () => { 6 | return delimList(emojis as unknown as string[]); 7 | }; 8 | -------------------------------------------------------------------------------- /server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module" 6 | }, 7 | "extends": ["@leftonread/eslint-config"], 8 | "rules": {} 9 | } 10 | -------------------------------------------------------------------------------- /web/src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | const basicEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 2 | 3 | export function isValidEmail(email: string) { 4 | if (!basicEmailRegex.test(email)) { 5 | return false 6 | } 7 | 8 | return true 9 | } 10 | -------------------------------------------------------------------------------- /app/.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | *.eot binary 9 | *.otf binary 10 | *.ttf binary 11 | *.woff binary 12 | *.woff2 binary 13 | -------------------------------------------------------------------------------- /web/src/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export function usePrevious(value: T) { 4 | const ref = React.useRef() 5 | 6 | React.useEffect(() => { 7 | ref.current = value 8 | }, [value]) 9 | 10 | return ref.current 11 | } 12 | -------------------------------------------------------------------------------- /web/src/components/types.ts: -------------------------------------------------------------------------------- 1 | export interface Avatar { 2 | source: string 3 | color: string 4 | } 5 | 6 | export interface IText { 7 | key: number 8 | name: string 9 | text: string 10 | length: number 11 | avatar: Avatar 12 | words?: Array 13 | } 14 | -------------------------------------------------------------------------------- /app/src/constants/types.ts: -------------------------------------------------------------------------------- 1 | export type NotificationSettings = { 2 | responseRemindersEnabled: boolean; 3 | }; 4 | 5 | export type ScheduledMessage = { 6 | id: string; 7 | message: string; 8 | phoneNumber: string; 9 | contactName: string; 10 | sendDate: Date; 11 | }; 12 | -------------------------------------------------------------------------------- /app/src/constants/reactions.ts: -------------------------------------------------------------------------------- 1 | import { delimList } from '../utils/delimList'; 2 | import { lowerCaseList } from '../utils/lowerCaseList'; 3 | 4 | const reactionsList = ['Laughed', 'Loved', 'Emphasized', 'Disliked', 'Liked']; 5 | export const reactions = delimList(lowerCaseList(reactionsList)); 6 | -------------------------------------------------------------------------------- /app/src/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | import { render } from '@testing-library/react'; 4 | 5 | import { App } from '../components/App'; 6 | 7 | describe('App', () => { 8 | it('should render', () => { 9 | expect(render()).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "preset-react": { 7 | "runtime": "automatic", 8 | "importSource": "@emotion/react" 9 | } 10 | } 11 | ] 12 | ], 13 | "plugins": ["@emotion/babel-plugin", "inline-react-svg"] 14 | } 15 | -------------------------------------------------------------------------------- /web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testPathIgnorePatterns: ['/.next/', '/node_modules/'], 3 | setupFilesAfterEnv: ['/setUpJest.js'], 4 | moduleNameMapper: { 5 | '\\.(css|scss)$': 'identity-obj-proxy', 6 | '\\.(png|svg|pdf|jpg|jpeg)$': '/src/__mocks__/fileMock.js', 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /app/src/renderer/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Left on Read 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app/.erb/scripts/delete-source-maps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | export default function deleteSourceMaps() { 7 | rimraf.sync(path.join(webpackPaths.distMainPath, '*.js.map')); 8 | rimraf.sync(path.join(webpackPaths.distRendererPath, '*.js.map')); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/LiveDb/getRespondReminders.ts: -------------------------------------------------------------------------------- 1 | import { queryRespondReminders } from '../../analysis/queries/RespondReminders'; 2 | import { initializeLiveDb } from './initializeLiveDb'; 3 | 4 | export async function getLiveRespondReminders() { 5 | const db = await initializeLiveDb(); 6 | 7 | const reminders = await queryRespondReminders(db); 8 | 9 | return reminders; 10 | } 11 | -------------------------------------------------------------------------------- /web/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #FFFFFF 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/.erb/scripts/link-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import webpackPaths from '../configs/webpack.paths'; 4 | 5 | const { srcNodeModulesPath } = webpackPaths; 6 | const { appNodeModulesPath } = webpackPaths; 7 | 8 | if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) { 9 | fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction'); 10 | } 11 | -------------------------------------------------------------------------------- /web/setUpJest.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | 3 | jest.mock('./src/utils/gtag') 4 | 5 | if (!SVGElement.prototype.getTotalLength) { 6 | SVGElement.prototype.getTotalLength = () => 1 7 | } 8 | 9 | global.matchMedia = 10 | global.matchMedia || 11 | function () { 12 | return { 13 | addListener: jest.fn(), 14 | removeListener: jest.fn(), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/components/Filters/LimitFilter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LimitFilterProps { 4 | limit: number; 5 | handleChange: (event: React.ChangeEvent) => void; 6 | } 7 | 8 | export function LimitFilter(props: LimitFilterProps) { 9 | const { limit, handleChange } = props; 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /app/assets/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/utils/files.ts: -------------------------------------------------------------------------------- 1 | import copy from 'recursive-copy'; 2 | 3 | /** 4 | * TREAD WITH CAUTION: this is a very dangerous function. 5 | * Overwrites files. 6 | * @param originalPath 7 | * @param appPath 8 | */ 9 | export async function copyFiles( 10 | originalPath: string, 11 | appPath: string 12 | ): Promise { 13 | await copy(originalPath, appPath, { 14 | overwrite: true, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /web/src/utils/addContact.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const API_URL = 4 | process.env.NODE_ENV === 'production' 5 | ? 'https://leftonread.herokuapp.com/api' 6 | : 'http://localhost:8080/api' 7 | 8 | export const addContact = async ({ 9 | email, 10 | type, 11 | }: { 12 | email: string 13 | type: string 14 | }) => { 15 | return await axios.post(`${API_URL}/contact`, { email, type }) 16 | } 17 | -------------------------------------------------------------------------------- /app/src/utils/analytics.ts: -------------------------------------------------------------------------------- 1 | // Helper function for the renderer 2 | import { ipcRenderer } from 'electron'; 3 | 4 | /** 5 | * ONLY WORKS IN THE RENDERER 6 | * DOES NOT WORK IN MAIN 7 | */ 8 | export function logEvent({ 9 | eventName, 10 | properties, 11 | }: { 12 | eventName: string; 13 | properties?: Record; 14 | }) { 15 | ipcRenderer.invoke('log-event', eventName, properties); 16 | } 17 | -------------------------------------------------------------------------------- /web/src/components/DefaultContentContainer.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/react' 2 | 3 | export function DefaultContentContainer({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 17 | {children} 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | ## Left on Read Server 2 | 3 | Our server's only purpose is to distribute license keys (via email) and to contact support (also via email). You will see that no text messages are read by our server. 4 | 5 | ## Getting started. 6 | 7 | To start, run `yarn dev`. 8 | 9 | Make sure you have `.env` file in the project root with the approriate values or placeholders. 10 | 11 | ### Deployment 12 | 13 | The production deployment is hosted on Heroku. 14 | -------------------------------------------------------------------------------- /app/.erb/scripts/check-node-env.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function checkNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/components/Dashboard/Wrapped/Watermark.tsx: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from 'framer-motion'; 2 | 3 | import LogoWithText from '../../../../assets/LogoWithText.svg'; 4 | 5 | export function Watermark() { 6 | return ( 7 | 8 | 9 | Left on Read 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/constants/punctuation.ts: -------------------------------------------------------------------------------- 1 | // TODO(Danilowicz): this should be a regex instead 2 | import { delimList } from '../utils/delimList'; 3 | 4 | const punctuationList = [ 5 | '?', 6 | '-', 7 | '—', 8 | '.', 9 | ',', 10 | '~', 11 | `'`, 12 | '#', 13 | '$', 14 | '%', 15 | '^', 16 | '&', 17 | '*', 18 | '(', 19 | ')', 20 | ':', 21 | ';', 22 | '!', 23 | '--', 24 | '---', 25 | '—', 26 | ]; 27 | export const punctuation = delimList(punctuationList); 28 | -------------------------------------------------------------------------------- /web/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "settings": { 11 | "react": { 12 | "version": "detect" 13 | } 14 | }, 15 | "extends": ["@leftonread/eslint-config", "plugin:react/recommended"], 16 | "rules": { 17 | "react/react-in-jsx-scope": "off", 18 | "react/display-name": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/contact_us.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Contact us 3 | about: Use this form to reach out about absolute anything! 4 | title: '' 5 | labels: contact-us 6 | assignees: alexdanilowicz, Teddarific 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/.erb/scripts/clean.js: -------------------------------------------------------------------------------- 1 | import process from 'process'; 2 | import rimraf from 'rimraf'; 3 | 4 | import webpackPaths from '../configs/webpack.paths'; 5 | 6 | const args = process.argv.slice(2); 7 | const commandMap = { 8 | dist: webpackPaths.distPath, 9 | release: webpackPaths.releasePath, 10 | dll: webpackPaths.dllPath, 11 | }; 12 | 13 | args.forEach((x) => { 14 | const pathToRemove = commandMap[x]; 15 | if (pathToRemove !== undefined) { 16 | rimraf.sync(pathToRemove); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /app/src/renderer/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { Channels } from 'main/preload'; 2 | 3 | declare global { 4 | interface Window { 5 | electron: { 6 | ipcRenderer: { 7 | sendMessage(channel: Channels, args: unknown[]): void; 8 | on( 9 | channel: string, 10 | func: (...args: unknown[]) => void 11 | ): (() => void) | undefined; 12 | once(channel: string, func: (...args: unknown[]) => void): void; 13 | }; 14 | }; 15 | } 16 | } 17 | 18 | export {}; 19 | -------------------------------------------------------------------------------- /web/next.config.js: -------------------------------------------------------------------------------- 1 | const withImages = require('next-images') 2 | 3 | module.exports = { 4 | ...withImages, 5 | images: { 6 | disableStaticImages: true, 7 | }, 8 | async redirects() { 9 | return [ 10 | { 11 | source: '/download', 12 | destination: '/?ref=download', 13 | permanent: false, 14 | }, 15 | { 16 | source: '/wrapped', 17 | destination: '/?ref=wrapped', 18 | permanent: false, 19 | }, 20 | ] 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /app/.erb/scripts/check-port-in-use.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 npm start` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leftonread/eslint-config", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "peerDependencies": { 12 | "@typescript-eslint/eslint-plugin": "^4.15.2", 13 | "@typescript-eslint/parser": "^4.15.2", 14 | "eslint": "^7.21.0", 15 | "eslint-plugin-prettier": "^3.3.1", 16 | "eslint-plugin-simple-import-sort": "^7.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/components/Premium/GoldContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | export type TGoldContext = { 4 | isPremium: boolean; 5 | setPremium: (arg0: boolean) => void; 6 | }; 7 | 8 | export const GoldContext = React.createContext({ 9 | isPremium: false, 10 | setPremium: () => {}, 11 | }); 12 | 13 | export function useGoldContext() { 14 | const context = useContext(GoldContext); 15 | 16 | if (context === undefined) { 17 | throw new Error('Gold Context is undefined'); 18 | } 19 | 20 | return context; 21 | } 22 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # chat.db files 32 | *.db-shm 33 | *.db-wal 34 | *.db -------------------------------------------------------------------------------- /app/src/components/Loaders/GraphContainerLoading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from '@chakra-ui/react'; 2 | 3 | export function GraphContainerLoading() { 4 | return ( 5 | <> 6 |
13 | 14 | 15 |
16 |
17 | 18 |
19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /web/src/components/HighlightedText.tsx: -------------------------------------------------------------------------------- 1 | // function hexToRGBA(hex: string, alpha: number | string) { 2 | // const r = parseInt(hex.slice(1, 3), 16), 3 | // g = parseInt(hex.slice(3, 5), 16), 4 | // b = parseInt(hex.slice(5, 7), 16) 5 | 6 | // if (alpha) { 7 | // return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')' 8 | // } else { 9 | // return 'rgb(' + r + ', ' + g + ', ' + b + ')' 10 | // } 11 | // } 12 | 13 | export default function HighlightedText({ 14 | text, 15 | }: { 16 | text: string 17 | color: string 18 | weight?: number 19 | }) { 20 | return {text} 21 | } 22 | -------------------------------------------------------------------------------- /app/.eslintignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | .eslintcache 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules 17 | 18 | # OSX 19 | .DS_Store 20 | 21 | release/app/dist 22 | release/build 23 | .erb/dll 24 | 25 | .idea 26 | npm-debug.log.* 27 | *.css.d.ts 28 | *.sass.d.ts 29 | *.scss.d.ts 30 | 31 | # eslint ignores hidden directories by default: 32 | # https://github.com/eslint/eslint/issues/8429 33 | !.erb -------------------------------------------------------------------------------- /app/release/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "left-on-read", 3 | "version": "4.0.1", 4 | "author": "Left on Read", 5 | "description": "A message analyzing platform", 6 | "main": "./dist/main/main.js", 7 | "scripts": { 8 | "electron-rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", 9 | "postinstall": "yarn electron-rebuild && npm run link-modules", 10 | "link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts" 11 | }, 12 | "dependencies": { 13 | "sqlite3": "^5.0.10", 14 | "v8-profiler-next": "1.6.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /web/src/__tests__/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | 3 | import Landing from '../pages/index' 4 | import PrivacyPolicy from '../pages/privacy' 5 | import TermsOfService from '../pages/terms' 6 | 7 | jest.mock('react-chartjs-2') 8 | 9 | describe('Page sanity checks', () => { 10 | it('renders Landing without crashing', () => { 11 | render() 12 | }) 13 | 14 | it('renders Privacy Policy without crashing', () => { 15 | render() 16 | }) 17 | 18 | it('renders Terms of Service without crashing', () => { 19 | render() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /eslint-config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['simple-import-sort'], 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaVersion: 2020, 6 | sourceType: 'module', 7 | }, 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | rules: { 13 | '@typescript-eslint/no-explicit-any': 'error', 14 | '@typescript-eslint/no-unused-vars': 'error', 15 | '@typescript-eslint/no-unused-expressions': 'error', 16 | '@typescript-eslint/explicit-module-boundary-types': 'off', 17 | 'simple-import-sort/imports': 'error', 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /web/src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | export default function Input({ 2 | placeholder, 3 | className, 4 | value, 5 | onChange, 6 | disabled, 7 | ...props 8 | }: { 9 | placeholder?: string 10 | className?: string 11 | value: string 12 | onChange?: (arg0: string) => void 13 | disabled?: boolean 14 | }) { 15 | return ( 16 | { 21 | if (onChange) { 22 | onChange(e.target.value) 23 | } 24 | }} 25 | disabled={disabled} 26 | {...props} 27 | /> 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "./dist", 7 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 8 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 9 | "strict": true /* Enable all strict type-checking options. */, 10 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 11 | }, 12 | "include": ["src/**/*.ts", "tests/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /app/assets/assets.d.ts: -------------------------------------------------------------------------------- 1 | type Styles = Record; 2 | 3 | declare module '*.svg' { 4 | const content: string; 5 | export default content; 6 | } 7 | 8 | declare module '*.png' { 9 | const content: string; 10 | export default content; 11 | } 12 | 13 | declare module '*.jpg' { 14 | const content: string; 15 | export default content; 16 | } 17 | 18 | declare module '*.scss' { 19 | const content: Styles; 20 | export default content; 21 | } 22 | 23 | declare module '*.sass' { 24 | const content: Styles; 25 | export default content; 26 | } 27 | 28 | declare module '*.css' { 29 | const content: Styles; 30 | export default content; 31 | } 32 | -------------------------------------------------------------------------------- /app/src/components/LoadingEllipsis.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export function LoadingEllipsis() { 5 | const [numEllipsis, setNumEllipsis] = useState(0); 6 | 7 | useEffect(() => { 8 | const timer = setInterval(() => { 9 | if (numEllipsis === 3) { 10 | setNumEllipsis(0); 11 | } else { 12 | setNumEllipsis(numEllipsis + 1); 13 | } 14 | }, 500); 15 | 16 | return () => { 17 | clearInterval(timer); 18 | }; 19 | }, [setNumEllipsis, numEllipsis]); 20 | 21 | return {Array(numEllipsis).fill('.').join('')}; 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /web/src/utils/firestore.ts: -------------------------------------------------------------------------------- 1 | import 'firebase/firestore' 2 | 3 | import firebase from 'firebase/app' 4 | 5 | const firebaseConfig = { 6 | apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, 7 | authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, 8 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 9 | } 10 | 11 | let db: firebase.firestore.Firestore | null = null 12 | 13 | export function initFirestore() { 14 | firebase.initializeApp(firebaseConfig) 15 | db = firebase.firestore() 16 | } 17 | 18 | export async function writeEmailToFirestore(email: string) { 19 | if (!db) { 20 | return 21 | } 22 | return db.collection('email').add({ 23 | email, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme, theme as baseTheme } from '@chakra-ui/react'; 2 | 3 | // SEE THEME COLOURS HERE: https://chakra-ui.com/docs/styled-system/theme 4 | export const theme = extendTheme({ 5 | colors: { 6 | primary: baseTheme.colors.purple, 7 | }, 8 | fonts: { 9 | body: `'Montserrat', sans-serif`, 10 | heading: `'Montserrat', sans-serif`, 11 | }, 12 | components: { 13 | Button: { 14 | baseStyle: { 15 | fontWeight: 500, 16 | fontSize: 14, 17 | }, 18 | variants: { 19 | primary: { 20 | backgroundColor: 'purple.100', 21 | _hover: { background: 'purple.200' }, 22 | }, 23 | }, 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /app/.erb/scripts/electron-rebuild.js: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import fs from 'fs'; 3 | 4 | import { dependencies } from '../../release/app/package.json'; 5 | import webpackPaths from '../configs/webpack.paths'; 6 | 7 | if ( 8 | Object.keys(dependencies || {}).length > 0 && 9 | fs.existsSync(webpackPaths.appNodeModulesPath) 10 | ) { 11 | const electronRebuildCmd = 12 | '../../node_modules/.bin/electron-rebuild --force --types prod,dev,optional --module-dir .'; 13 | const cmd = 14 | process.platform === 'win32' 15 | ? electronRebuildCmd.replace(/\//g, '\\') 16 | : electronRebuildCmd; 17 | execSync(cmd, { 18 | cwd: webpackPaths.appPath, 19 | stdio: 'inherit', 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /web/src/utils/validation.test.ts: -------------------------------------------------------------------------------- 1 | import { isValidEmail } from './validation' 2 | 3 | describe('test isValidEmail', () => { 4 | it('should accept valid emails', () => { 5 | expect(isValidEmail('test@gmail.com')).toBe(true) 6 | expect(isValidEmail('ted.darific@hotmail.net')).toBe(true) 7 | expect(isValidEmail('ted13_+12@yahoo.com')).toBe(true) 8 | expect(isValidEmail('132312@bil.org')).toBe(true) 9 | }) 10 | 11 | it('should fail invalid emails', () => { 12 | expect(isValidEmail('test')).toBe(false) 13 | expect(isValidEmail('test@')).toBe(false) 14 | expect(isValidEmail('test@gmail')).toBe(false) 15 | expect(isValidEmail('@gmail.com')).toBe(false) 16 | expect(isValidEmail('@')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /app/src/analysis/queries/InboxWriteQuery.ts: -------------------------------------------------------------------------------- 1 | import log from 'electron-log'; 2 | import * as sqlite3 from 'sqlite3'; 3 | 4 | import { runP } from '../../utils/sqliteWrapper'; 5 | import { CoreTableNames } from '../tables/types'; 6 | 7 | export async function queryInboxWrite( 8 | db: sqlite3.Database, 9 | chatId: string 10 | ): Promise { 11 | // For now, just use the service_center column which is unused. In the future, we'll use a different column 12 | // Also, we should update every single row. The update is chat based, but right now table is message based 13 | const q = ` 14 | UPDATE ${CoreTableNames.CORE_MAIN_TABLE} SET service_center = ${chatId} WHERE chat_id = ${chatId} 15 | `; 16 | 17 | return runP(db, q); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/constants/filters.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_LIMIT = 15; 2 | 3 | export enum GroupChatFilters { 4 | BOTH = 'Both Individual and Group Chats', 5 | ONLY_INDIVIDUAL = 'Only Individual Conversations', 6 | } 7 | 8 | export type TimeRangeFilters = { 9 | startDate: Date; 10 | endDate?: Date; // uses tomorrow by default 11 | }; 12 | 13 | // TODO(Danilowicz) this should leverage constants/reactions 14 | export function filterOutReactions(): string { 15 | return `( 16 | LOWER(text) NOT LIKE "emphasized%" 17 | AND LOWER(text) NOT LIKE "emphasised%" 18 | AND LOWER(text) NOT LIKE "loved%" 19 | AND LOWER(text) NOT LIKE "liked%" 20 | AND LOWER(text) NOT LIKE "disliked%" 21 | AND LOWER(text) NOT LIKE "laughed%" 22 | )`; 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Desktop (please complete the following information):** 26 | - OS: [e.g. iOS] 27 | - Version [e.g. 22] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /web/src/utils/gtag.ts: -------------------------------------------------------------------------------- 1 | export const GA_TRACKING_ID = 'UA-113056721-2' 2 | 3 | export function pageView(url: string) { 4 | window.gtag('config', GA_TRACKING_ID, { 5 | page_path: url, 6 | }) 7 | } 8 | 9 | export function logEvent({ 10 | action, 11 | category, 12 | label = '', 13 | value = 0, 14 | }: { 15 | action: string 16 | category: string 17 | label?: string 18 | value?: number 19 | }) { 20 | window.gtag('event', action, { 21 | event_category: category, 22 | event_label: label, 23 | value, 24 | }) 25 | } 26 | 27 | export function logException({ 28 | description, 29 | fatal, 30 | }: { 31 | description?: string 32 | fatal?: boolean 33 | }) { 34 | window.gtag('event', 'exception', { 35 | description, 36 | fatal, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /app/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Electron: Main", 6 | "type": "node", 7 | "request": "launch", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "npm", 10 | "runtimeArgs": ["run", "start"], 11 | "env": { 12 | "MAIN_ARGS": "--inspect=5858 --remote-debugging-port=9223" 13 | } 14 | }, 15 | { 16 | "name": "Electron: Renderer", 17 | "type": "chrome", 18 | "request": "attach", 19 | "port": 9223, 20 | "webRoot": "${workspaceFolder}", 21 | "timeout": 15000 22 | } 23 | ], 24 | "compounds": [ 25 | { 26 | "name": "Electron: All", 27 | "configurations": ["Electron: Main", "Electron: Renderer"] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".eslintrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".eslintignore": "ignore" 6 | }, 7 | 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "html", 12 | "typescriptreact" 13 | ], 14 | 15 | "javascript.validate.enable": false, 16 | "javascript.format.enable": false, 17 | "typescript.format.enable": false, 18 | 19 | "search.exclude": { 20 | ".git": true, 21 | ".eslintcache": true, 22 | ".erb/dll": true, 23 | "release/{build,app/dist}": true, 24 | "node_modules": true, 25 | "npm-debug.log.*": true, 26 | "test/**/__snapshots__": true, 27 | "package-lock.json": true, 28 | "*.{css,sass,scss}.d.ts": true 29 | }, 30 | "editor.formatOnSave": true 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; 2 | 3 | export type Channels = 'ipc-example'; 4 | 5 | contextBridge.exposeInMainWorld('electron', { 6 | ipcRenderer: { 7 | sendMessage(channel: Channels, args: unknown[]) { 8 | ipcRenderer.send(channel, args); 9 | }, 10 | on(channel: Channels, func: (...args: unknown[]) => void) { 11 | const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => 12 | func(...args); 13 | ipcRenderer.on(channel, subscription); 14 | 15 | return () => ipcRenderer.removeListener(channel, subscription); 16 | }, 17 | once(channel: Channels, func: (...args: unknown[]) => void) { 18 | ipcRenderer.once(channel, (_event, ...args) => func(...args)); 19 | }, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /server/src/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import multer from 'multer' 3 | 4 | import * as ContactsController from './controllers/ContactsController' 5 | import * as HelpController from './controllers/HelpController' 6 | import * as PremiumController from './controllers/PremiumController' 7 | 8 | const upload = multer({ dest: './uploads' }) 9 | 10 | export const LorRouter = express.Router() 11 | 12 | LorRouter.post( 13 | '/help', 14 | upload.single('logFile'), 15 | HelpController.handleHelpRequest 16 | ) 17 | 18 | LorRouter.post( 19 | '/webhooks/stripe', 20 | express.raw({ type: 'application/json' }), 21 | PremiumController.handleStripeWebhookEvent 22 | ) 23 | 24 | LorRouter.post('/contact', ContactsController.addContact) 25 | 26 | LorRouter.post('/activate', PremiumController.verifyLicenseKey) 27 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "noImplicitAny": true, 21 | "noImplicitThis": true, 22 | "strictNullChecks": true, 23 | "incremental": true 24 | }, 25 | "exclude": [ 26 | "node_modules" 27 | ], 28 | "include": [ 29 | "next-env.d.ts", 30 | "**/*.ts", 31 | "**/*.tsx", 32 | "src/custom.d.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /app/.erb/scripts/check-build-exists.ts: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import chalk from 'chalk'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | import webpackPaths from '../configs/webpack.paths'; 7 | 8 | const mainPath = path.join(webpackPaths.distMainPath, 'main.js'); 9 | const rendererPath = path.join(webpackPaths.distRendererPath, 'renderer.js'); 10 | 11 | if (!fs.existsSync(mainPath)) { 12 | throw new Error( 13 | chalk.whiteBright.bgRed.bold( 14 | 'The main process is not built yet. Build it by running "npm run build:main"' 15 | ) 16 | ); 17 | } 18 | 19 | if (!fs.existsSync(rendererPath)) { 20 | throw new Error( 21 | chalk.whiteBright.bgRed.bold( 22 | 'The renderer process is not built yet. Build it by running "npm run build:renderer"' 23 | ) 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2021", 5 | "module": "commonjs", 6 | "lib": ["dom", "es2021"], 7 | "declaration": true, 8 | "declarationMap": true, 9 | "jsx": "react-jsx", 10 | "strict": true, 11 | "pretty": true, 12 | "sourceMap": true, 13 | "baseUrl": "./src", 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "moduleResolution": "node", 19 | "esModuleInterop": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "allowJs": true, 23 | "outDir": "release/app/dist", 24 | "skipLibCheck": true 25 | }, 26 | "exclude": ["test", "release/build", "release/app/dist", ".erb/dll"] 27 | } 28 | -------------------------------------------------------------------------------- /app/src/components/Dashboard/GlobalContext.tsx: -------------------------------------------------------------------------------- 1 | import { ContactOptionsQueryResult } from 'analysis/queries/ContactOptionsQuery'; 2 | import React, { useContext } from 'react'; 3 | 4 | export type TDateRange = { 5 | earliestDate: Date; 6 | latestDate: Date; 7 | }; 8 | 9 | export type TGlobalContext = { 10 | isLoading: boolean; 11 | contacts?: ContactOptionsQueryResult[]; 12 | dateRange: TDateRange; 13 | }; 14 | 15 | export const GlobalContext = React.createContext({ 16 | isLoading: true, 17 | contacts: [], 18 | dateRange: { 19 | earliestDate: new Date(), 20 | latestDate: new Date(), 21 | }, 22 | }); 23 | 24 | export function useGlobalContext() { 25 | const context = useContext(GlobalContext); 26 | 27 | if (context === undefined) { 28 | throw new Error('Context is undefined'); 29 | } 30 | 31 | return context; 32 | } 33 | -------------------------------------------------------------------------------- /app/src/components/Home/Redirecter.tsx: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import log from 'electron-log'; 3 | import { useEffect } from 'react'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | import { APP_VERSION } from '../../constants/versions'; 7 | 8 | export function Redirecter() { 9 | const navigate = useNavigate(); 10 | useEffect(() => { 11 | const checkExistence = async () => { 12 | let doesExist = false; 13 | 14 | try { 15 | doesExist = await ipcRenderer.invoke('check-initialized'); 16 | } catch (e) { 17 | log.error(e); 18 | } 19 | if (doesExist) { 20 | navigate('/dashboard'); 21 | } else { 22 | navigate('/start'); 23 | } 24 | }; 25 | 26 | log.info(`Started up on version ${APP_VERSION}`); 27 | checkExistence(); 28 | }, [navigate]); 29 | 30 | return
; 31 | } 32 | -------------------------------------------------------------------------------- /web/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Footer } from '../components/Footer' 4 | import { Download } from '../components/sections/Download' 5 | import { GetStarted } from '../components/sections/GetStarted' 6 | import { Infographic } from '../components/sections/Infographic' 7 | import { Security } from '../components/sections/Security' 8 | import { Wrapped } from '../components/sections/Wrapped' 9 | 10 | // TODO(teddy): Add a minheight 11 | export default function Landing() { 12 | const ctaRef = React.useRef(null) 13 | 14 | // NOTE(teddy): Important we don't wrap with a div so that the footer is a direct child of the body tag. 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | 22 |