diff --git a/package.json b/package.json index 332a3d9..2d580bb 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "blueimp-load-image": "^2.24.0", "firebase": "^9.6.2", + "guesslanguage": "^0.2.0", "node-sass": "^4.13.0", "react": "^16.11.0", "react-dom": "^16.11.0", diff --git a/src/Board/index.tsx b/src/Board/index.tsx index 6028a6c..a26bcf3 100644 --- a/src/Board/index.tsx +++ b/src/Board/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { RouteComponentProps } from 'react-router'; import { logEvent } from 'firebase/analytics'; import './board.scss'; @@ -7,6 +7,7 @@ import { useAnalytics, useDatabase } from '../Provider/FirebaseApp'; import Dialog from './Dialog'; import { Background } from './Background'; import { subscribePost } from './subscribePost'; +import { estimateReadingTime } from '../utils/estimateReadingTime'; const Board: React.FC = (props) => { // TODO: remove this and find a proper filter way on GA @@ -16,29 +17,35 @@ const Board: React.FC = (props) => { }, [analytics]); const [modalDisplay, setModalDisplay] = useState(false); - const openModalAndClose = useCallback<(timeout?: number) => void>((timeout = 5000) => { + const openModalAndClose = useCallback<(onClose: () => any, timeout?: number) => void>((onClose, timeout = 5000) => { setModalDisplay(true); setTimeout(() => { setModalDisplay(false); + onClose(); }, timeout); }, [setModalDisplay]); const [post, setPost] = useState({}); const database = useDatabase(); + const { next, unsubscribe } = useMemo(() => subscribePost(database), [database]); + const getNextPost = useCallback(() => { + setTimeout(() => { + const newPost = next(); + setPost(newPost); + estimateReadingTime(newPost.greetings, (estimatedSecond) => { + const timeout = Math.max(estimatedSecond * 1000, 5000); + openModalAndClose(getNextPost, timeout); + }); + }, 3000); + }, [openModalAndClose, next]); useEffect(() => { - // subscribe post while component did mount - const unsubscribe = subscribePost(database)((newPost) => { - if (newPost) { - setPost(newPost); - openModalAndClose(); - } - }); + getNextPost(); return () => { // unsubscribe post while component will unmount unsubscribe(); }; - }, [database, openModalAndClose]); + }, [getNextPost, unsubscribe]); return ( diff --git a/src/Board/subscribePost.ts b/src/Board/subscribePost.ts index d4a2eb8..91f12e6 100644 --- a/src/Board/subscribePost.ts +++ b/src/Board/subscribePost.ts @@ -36,10 +36,10 @@ const heap = newHeap((node1, node2) => { return false; }); -type PostListener = (post: WeddiApp.Post.Data) => any; +type NextFn = () => WeddiApp.Post.Data; type UnsubscribeFn = () => void; -export const subscribePost = (database: Database) => (listener: PostListener): UnsubscribeFn => { +export const subscribePost = (database: Database): { unsubscribe: UnsubscribeFn; next: NextFn } => { const postsPool: {[timestampId: string]: WeddiApp.Post.Data} = {}; let feeds: HeapNode[] = []; listPosts(database).then(posts => { @@ -58,7 +58,7 @@ export const subscribePost = (database: Database) => (listener: PostListener): U }); }); - const interval = setInterval(() => { + const next: NextFn = () => { if (feeds.length === 0) { // refill feeds with pool feeds feeds = Object.keys(postsPool).map(postId => newHeapNode(1, postId)); @@ -66,13 +66,16 @@ export const subscribePost = (database: Database) => (listener: PostListener): U } const nextFeed = heap.pop(feeds); if (nextFeed) { - listener(postsPool[nextFeed.timestampId]); + return postsPool[nextFeed.timestampId]; } - }, 8000); + throw new Error('queue is empty'); + }; const unsubscribe: UnsubscribeFn = () => { - clearInterval(interval); // TODO: unsub API }; - return unsubscribe; + return { + unsubscribe, + next, + }; }; diff --git a/src/api/index.ts b/src/api/index.ts index 81f0958..7b88817 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -51,7 +51,7 @@ export const writePost = (database: Database, postData: WeddiApp.Post.UserInput) userAgent: navigator.userAgent, id: postId }; - return set(databaseRef(database, `${configService.config.post.namespace}/posts/${postId}}`), wrappedPostData); + return set(databaseRef(database, `${configService.config.post.namespace}/posts/${postId}`), wrappedPostData); }; export const onNewPost = (database: Database, callback: (post: WeddiApp.Post.Data) => any): void => { diff --git a/src/guessLanguage.d.ts b/src/guessLanguage.d.ts new file mode 100644 index 0000000..e922ed5 --- /dev/null +++ b/src/guessLanguage.d.ts @@ -0,0 +1,8 @@ +declare module 'guesslanguage' { + export namespace guessLanguage { + export function detect(input: string, callback: (language: string) => any): void; + export function name(input: string, callback: (languageName: string) => any): void; + export function code(input: string, callback: (languageIANA: string) => any): void; + export function info(input: string, callback: (languageInfo: [string, string, string]) => any): void; + } +} diff --git a/src/utils/estimateReadingTime.ts b/src/utils/estimateReadingTime.ts new file mode 100644 index 0000000..4e8f5af --- /dev/null +++ b/src/utils/estimateReadingTime.ts @@ -0,0 +1,34 @@ +import guessLanguage from 'guesslanguage'; + +// data from https://irisreading.com/average-reading-speed-in-various-languages/ +const CharactersPerMin = { + default: 987, + Arabic: 612, + Chinese: 255, + Dutch: 978, + English: 987, + Finnish: 1078, + French: 998, + German: 920, + Hebrew: 833, + Italian: 950, + Japanese: 357, + Polish: 916, + Portuguese: 913, + Russian: 986, + Slovenian: 885, + Spanish: 1025, + Swedish: 917, + Turkish: 1054, +}; + +export function estimateReadingTime(paragraph: string, callback: (estimatedSecond: number) => any): void { + guessLanguage.guessLanguage.name(paragraph, (languageName: string) => { + const readingSpeedCharPerMin = languageName in CharactersPerMin ? + CharactersPerMin[languageName as keyof typeof CharactersPerMin] : + CharactersPerMin.default; + + const second = Math.floor(paragraph.length / readingSpeedCharPerMin * 60); + callback(second); + }); +} diff --git a/yarn.lock b/yarn.lock index 893c81b..120a393 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5727,6 +5727,11 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= +guesslanguage@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/guesslanguage/-/guesslanguage-0.2.0.tgz#e5e4758a3d751ba4dd23f49a331dcf22e290038c" + integrity sha1-5eR1ij11G6TdI/SaMx3PIuKQA4w= + gzip-size@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"