1- import { sum } from 'lodash-es'
2- import { useState , useMemo , useEffect } from 'react'
1+ import { useState , useMemo , useEffect , type JSX } from 'react'
32import { makeStyles } from '@masknet/theme'
43import { Typography } from '@mui/material'
5- import { useRenderPhraseCallbackOnDepsChange } from '@masknet/shared-base-ui '
4+ import { isNonNull } from '@masknet/kit '
65
76const useStyles = makeStyles ( ) ( ( theme ) => ( {
87 typed : {
@@ -13,75 +12,82 @@ const useStyles = makeStyles()((theme) => ({
1312 color : theme . palette . maskColor . highlight ,
1413 } ,
1514 } ,
16- endTyping : {
15+ typing : {
1716 opacity : 0.5 ,
1817 } ,
1918} ) )
2019
2120interface OnboardingWriterProps extends withClasses < 'typed' | 'endTyping' > {
22- sentence : Array < string [ ] | undefined >
21+ sentence : Array < string | undefined >
2322}
24- let segmenter : Intl . Segmenter
2523export function OnboardingWriter ( { sentence, ...props } : OnboardingWriterProps ) {
2624 const { classes, cx } = useStyles ( undefined , { props } )
25+ const typing = cx ( classes . typing , classes . typed )
26+ const [ jsx , setJsx ] = useState < JSX . Element | undefined > ( undefined )
2727
28- const allChars = useMemo ( ( ) => sentence . flat ( ) . filter ( Boolean ) . join ( '' ) , [ sentence ] )
29- const allCharsSegmented = useMemo ( ( ) => {
30- return [ ...( segmenter ||= new Intl . Segmenter ( ) ) . segment ( allChars ) ] . map ( ( x ) => x . segment )
31- } , [ allChars ] )
32- const segmentCount = allCharsSegmented . length
33-
34- const [ codePointIndex , setCodePointIndex ] = useState ( 0 )
35- const [ segmentIndex , setSegmentIndex ] = useState ( 0 )
36- useRenderPhraseCallbackOnDepsChange ( ( ) => {
37- setCodePointIndex ( 0 )
38- setSegmentIndex ( 0 )
39- } , [ sentence ] )
28+ const writer = useMemo ( ( ) => onBoardingWriter ( { typed : classes . typed , typing } , sentence ) , [ sentence ] )
4029 useEffect ( ( ) => {
4130 const timer = setInterval ( ( ) => {
42- setSegmentIndex ( ( segmentIndex ) => {
43- const nextSegmentIndex = segmentIndex + 1
44- if ( segmentIndex > segmentCount ) {
45- clearInterval ( timer )
46- return segmentIndex
47- }
48- setCodePointIndex ( allCharsSegmented . slice ( 0 , nextSegmentIndex ) . join ( '' ) . length )
49- return nextSegmentIndex
50- } )
31+ const next = writer . next ( )
32+ if ( next . done ) {
33+ clearInterval ( timer )
34+ } else {
35+ setJsx ( next . value )
36+ }
5137 } , 50 )
5238
5339 return ( ) => {
5440 clearInterval ( timer )
5541 }
56- } , [ allCharsSegmented , segmentCount ] )
42+ } , [ writer ] )
5743
58- const jsx = useMemo ( ( ) => {
59- const newJsx = [ ]
60- let remain = codePointIndex
61- for ( const fragment of sentence ) {
62- if ( ! fragment ) continue
63- if ( remain <= 0 ) break
64- const size = sum ( fragment . map ( ( x ) => x . length ) )
65- const take = Math . min ( size , remain )
66-
67- remain -= take
68-
69- const [ text , strongText ] = fragment
44+ return jsx
45+ }
46+ let segmenter : Intl . Segmenter
47+ function * onBoardingWriter ( className : { typed : string ; typing : string } , sentences : Array < string | undefined > ) {
48+ segmenter ||= new Intl . Segmenter ( )
49+ const previousLines : JSX . Element [ ] = [ ]
50+ const currentLine : Array < { type : 'bold' | 'normal' ; text : string } > = [ { type : 'normal' , text : '' } ]
7051
71- const className = cx ( classes . typed , remain !== 0 ? classes . endTyping : undefined )
72- // the trailing space gets trimmed by i18n marco
73- if ( take <= text . length ) newJsx . push ( < Typography className = { className } > { text . slice ( 0 , take ) } </ Typography > )
74- else
75- newJsx . push (
76- < Typography className = { className } >
77- { text }
78- < strong key = { size } > { strongText . slice ( 0 , take - text . length ) } </ strong >
79- </ Typography > ,
80- )
52+ for ( const sentence of sentences . filter ( isNonNull ) ) {
53+ const chars = [ ...segmenter . segment ( sentence ) ]
54+ let currentLineJSX : JSX . Element | undefined
55+ for ( let index = 0 ; index < chars . length ; index += 1 ) {
56+ const char = chars [ index ] . segment
57+ const lastPiece = currentLine . at ( - 1 ) !
58+ if ( char === '*' ) {
59+ const nextChar = chars [ index + 1 ] ?. segment
60+ if ( nextChar === '*' ) {
61+ currentLine . push ( { type : lastPiece . type === 'normal' ? 'bold' : 'normal' , text : '' } )
62+ index += 1
63+ continue
64+ }
65+ }
66+ lastPiece . text += char
67+ const children = currentLine . map ( ( x , index ) =>
68+ x . type === 'normal' ? x . text : < strong key = { index } > { x . text } </ strong > ,
69+ )
70+ currentLineJSX = (
71+ < Typography className = { className . typed } key = { sentence } >
72+ { children }
73+ </ Typography >
74+ )
75+ yield (
76+ < >
77+ { previousLines }
78+ {
79+ < Typography className = { className . typing } key = { sentence } >
80+ { children }
81+ </ Typography >
82+ }
83+ </ >
84+ )
8185 }
82-
83- return newJsx
84- } , [ sentence , codePointIndex ] )
85-
86- return < > { jsx } </ >
86+ if ( currentLineJSX ) {
87+ previousLines . push ( currentLineJSX )
88+ currentLine . length = 0
89+ currentLine . push ( { type : 'normal' , text : '' } )
90+ }
91+ yield < > { previousLines } </ >
92+ }
8793}
0 commit comments