diff options
Diffstat (limited to 'app')
31 files changed, 654 insertions, 328 deletions
diff --git a/app/build.gradle b/app/build.gradle index e09740dd..1c06cee9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -142,6 +142,7 @@ dependencies { androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:${KOTLIN}" androidTestImplementation "com.android.support.test:rules:${TEST_RULE}" testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${KOTLIN}" + testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}" testImplementation "junit:junit:${JUNIT}" implementation "org.jetbrains.kotlin:kotlin-stdlib:${KOTLIN}" diff --git a/app/src/main/assets/css/core/_core_bg.scss b/app/src/main/assets/css/core/_core_bg.scss index ba92dd1c..9fe90443 100644 --- a/app/src/main/assets/css/core/_core_bg.scss +++ b/app/src/main/assets/css/core/_core_bg.scss @@ -4,7 +4,7 @@ body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, -._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, +._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, @@ -76,4 +76,9 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS ._2zh4::before, ._2ip_ ._2zh4::before, ._2ip_ ._15kk::before, ._2ip_ ._15kk + ._4u3j::before, ._58a0:before, ._43mh::before, ._43mh::after, ._1_-1::before, ._1kmv:after, ._1_ac:before { background: $divider !important; +} + +//fab +button ._v89 ._54k8._1fl1 { + background: $accent !important; }
\ No newline at end of file diff --git a/app/src/main/assets/css/core/_core_text.scss b/app/src/main/assets/css/core/_core_text.scss index 0e858102..bc4a4dc1 100644 --- a/app/src/main/assets/css/core/_core_text.scss +++ b/app/src/main/assets/css/core/_core_text.scss @@ -6,7 +6,7 @@ ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, -._18qg, ._1_ac, +._18qg, ._1_ac, ._529p, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, diff --git a/app/src/main/assets/css/core/core.css b/app/src/main/assets/css/core/core.css index 05c6afe1..04560385 100644 --- a/app/src/main/assets/css/core/core.css +++ b/app/src/main/assets/css/core/core.css @@ -1,4 +1,4 @@ -[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #d7b0d7 !important; } +[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._529p, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #d7b0d7 !important; } strong > a, ._15ks ._2q8z._2q8z, ._1e3e { color: #3b5998 !important; } @@ -8,7 +8,7 @@ a, ._5fpq { color: #d59ed5 !important; } #viewport { background: #451515 !important; } -body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: rgba(255, 0, 255, 0.02) !important; } +body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: rgba(255, 0, 255, 0.02) !important; } ._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo { background: #496296 !important; } @@ -28,6 +28,8 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS ._220g, ._1_y8:after, ._2zh4::before, ._2ip_ ._2zh4::before, ._2ip_ ._15kk::before, ._2ip_ ._15kk + ._4u3j::before, ._58a0:before, ._43mh::before, ._43mh::after, ._1_-1::before, ._1kmv:after, ._1_ac:before { background: rgba(215, 176, 215, 0.3) !important; } +button ._v89 ._54k8._1fl1 { background: #3b5998 !important; } + ._15kl::before, ._37fd .inlineComposerButton, ._1hb:before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(215, 176, 215, 0.3) !important; } ._4_d1 { border-right: 1px solid rgba(215, 176, 215, 0.3) !important; } diff --git a/app/src/main/assets/css/themes/custom.css b/app/src/main/assets/css/themes/custom.css index 340e9139..c4fd8c7c 100644 --- a/app/src/main/assets/css/themes/custom.css +++ b/app/src/main/assets/css/themes/custom.css @@ -1,4 +1,4 @@ -[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: $T$ !important; } +[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._529p, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: $T$ !important; } strong > a, ._15ks ._2q8z._2q8z, ._1e3e { color: $A$ !important; } @@ -8,7 +8,7 @@ a, ._5fpq { color: $TT$ !important; } #viewport { background: $B$ !important; } -body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: $BT$ !important; } +body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: $BT$ !important; } ._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo { background: $C$ !important; } @@ -28,6 +28,8 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS ._220g, ._1_y8:after, ._2zh4::before, ._2ip_ ._2zh4::before, ._2ip_ ._15kk::before, ._2ip_ ._15kk + ._4u3j::before, ._58a0:before, ._43mh::before, ._43mh::after, ._1_-1::before, ._1kmv:after, ._1_ac:before { background: $D$ !important; } +button ._v89 ._54k8._1fl1 { background: $A$ !important; } + ._15kl::before, ._37fd .inlineComposerButton, ._1hb:before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid $D$ !important; } ._4_d1 { border-right: 1px solid $D$ !important; } diff --git a/app/src/main/assets/css/themes/material_amoled.css b/app/src/main/assets/css/themes/material_amoled.css index fd0d4026..8cfae227 100644 --- a/app/src/main/assets/css/themes/material_amoled.css +++ b/app/src/main/assets/css/themes/material_amoled.css @@ -1,4 +1,4 @@ -[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #fff !important; } +[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._529p, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #fff !important; } strong > a, ._15ks ._2q8z._2q8z, ._1e3e { color: #5d86dd !important; } @@ -8,7 +8,7 @@ a, ._5fpq { color: #eee !important; } #viewport { background: #000 !important; } -body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: #000 !important; } +body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: #000 !important; } ._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo { background: rgba(0, 0, 0, 0.35) !important; } @@ -28,6 +28,8 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS ._220g, ._1_y8:after, ._2zh4::before, ._2ip_ ._2zh4::before, ._2ip_ ._15kk::before, ._2ip_ ._15kk + ._4u3j::before, ._58a0:before, ._43mh::before, ._43mh::after, ._1_-1::before, ._1kmv:after, ._1_ac:before { background: rgba(255, 255, 255, 0.3) !important; } +button ._v89 ._54k8._1fl1 { background: #5d86dd !important; } + ._15kl::before, ._37fd .inlineComposerButton, ._1hb:before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(255, 255, 255, 0.3) !important; } ._4_d1 { border-right: 1px solid rgba(255, 255, 255, 0.3) !important; } diff --git a/app/src/main/assets/css/themes/material_dark.css b/app/src/main/assets/css/themes/material_dark.css index ce5e00bf..fc57ac30 100644 --- a/app/src/main/assets/css/themes/material_dark.css +++ b/app/src/main/assets/css/themes/material_dark.css @@ -1,4 +1,4 @@ -[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #fff !important; } +[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._529p, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #fff !important; } strong > a, ._15ks ._2q8z._2q8z, ._1e3e { color: #5d86dd !important; } @@ -8,7 +8,7 @@ a, ._5fpq { color: #eee !important; } #viewport { background: #303030 !important; } -body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: #303030 !important; } +body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: #303030 !important; } ._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo { background: #353535 !important; } @@ -28,6 +28,8 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS ._220g, ._1_y8:after, ._2zh4::before, ._2ip_ ._2zh4::before, ._2ip_ ._15kk::before, ._2ip_ ._15kk + ._4u3j::before, ._58a0:before, ._43mh::before, ._43mh::after, ._1_-1::before, ._1kmv:after, ._1_ac:before { background: rgba(255, 255, 255, 0.3) !important; } +button ._v89 ._54k8._1fl1 { background: #5d86dd !important; } + ._15kl::before, ._37fd .inlineComposerButton, ._1hb:before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(255, 255, 255, 0.3) !important; } ._4_d1 { border-right: 1px solid rgba(255, 255, 255, 0.3) !important; } diff --git a/app/src/main/assets/css/themes/material_glass.css b/app/src/main/assets/css/themes/material_glass.css index 695a2489..7d79dc78 100644 --- a/app/src/main/assets/css/themes/material_glass.css +++ b/app/src/main/assets/css/themes/material_glass.css @@ -1,4 +1,4 @@ -[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #fff !important; } +[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._529p, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #fff !important; } strong > a, ._15ks ._2q8z._2q8z, ._1e3e { color: #5d86dd !important; } @@ -8,7 +8,7 @@ a, ._5fpq { color: #eee !important; } #viewport { background: rgba(0, 0, 0, 0.1) !important; } -body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: transparent !important; } +body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: transparent !important; } ._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo { background: rgba(0, 0, 0, 0.25) !important; } @@ -28,6 +28,8 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS ._220g, ._1_y8:after, ._2zh4::before, ._2ip_ ._2zh4::before, ._2ip_ ._15kk::before, ._2ip_ ._15kk + ._4u3j::before, ._58a0:before, ._43mh::before, ._43mh::after, ._1_-1::before, ._1kmv:after, ._1_ac:before { background: rgba(255, 255, 255, 0.3) !important; } +button ._v89 ._54k8._1fl1 { background: #5d86dd !important; } + ._15kl::before, ._37fd .inlineComposerButton, ._1hb:before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(255, 255, 255, 0.3) !important; } ._4_d1 { border-right: 1px solid rgba(255, 255, 255, 0.3) !important; } diff --git a/app/src/main/assets/css/themes/material_light.css b/app/src/main/assets/css/themes/material_light.css index 7026cfe9..3e3ff551 100644 --- a/app/src/main/assets/css/themes/material_light.css +++ b/app/src/main/assets/css/themes/material_light.css @@ -1,4 +1,4 @@ -[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #000 !important; } +[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5, ._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk, .touched *, ._1_yj, ._1_yl, ._4pj9, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._50vk, ._1m2u, ._31y7, ._4kcb, ._1lf6, ._1lf5, ._1lf4, ._1hiz, ._xod, ._5ag5, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, ._529p, ._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782, ._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._2new, .appCenterCategorySelectorButton, div.sharerSelector, .footer, .mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw, ._5-7t, .fcl, ._4qas, .thread-title, .title, ._46pa, ._336p, ._1rrd, ._2om4, ._3m1m, ._2om2, ._5n_e, .appListExplanation, ._5yt8, ._8he, ._2luw, ._5rgs, h1, h2, h3, h4, h5, h6 { color: #000 !important; } strong > a, ._15ks ._2q8z._2q8z, ._1e3e { color: #3b5998 !important; } @@ -8,7 +8,7 @@ a, ._5fpq { color: #111 !important; } #viewport { background: #fafafa !important; } -body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: #fafafa !important; } +body, #root, #header, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, .btnD, .btnI, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._2q7u, ._2q7v, ._577z, ._2u4w, ._3u9p, ._3u9t, ._cw4, ._5_y-, ._5_y_, ._5_z3, ._cwy, ._5_z0, ._5_z1, ._5_z2, ._2mtc, ._206a, ._1_-1, ._1ybg, .appCenterCategorySelectorButton, ._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5, ._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, .acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3f50, .mentions-placeholder, .mentions, .mentions-shadow, .mentions-measurer, .acg, ._59tu, ._52z5, ._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1, ._35au, ._cwy, ._1rfn ._1rfk ._4vc-, ._1rfk, ._1rfk ._2v9s, ._301x { background: #fafafa !important; } ._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo { background: #fff !important; } @@ -28,6 +28,8 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS ._220g, ._1_y8:after, ._2zh4::before, ._2ip_ ._2zh4::before, ._2ip_ ._15kk::before, ._2ip_ ._15kk + ._4u3j::before, ._58a0:before, ._43mh::before, ._43mh::after, ._1_-1::before, ._1kmv:after, ._1_ac:before { background: rgba(0, 0, 0, 0.3) !important; } +button ._v89 ._54k8._1fl1 { background: #3b5998 !important; } + ._15kl::before, ._37fd .inlineComposerButton, ._1hb:before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(0, 0, 0, 0.3) !important; } ._4_d1 { border-right: 1px solid rgba(0, 0, 0, 0.3) !important; } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 80d248bc..8f932a94 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -58,6 +58,7 @@ import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL import com.pitchedapps.frost.fragments.BaseFragment +import com.pitchedapps.frost.parsers.FrostSearch import com.pitchedapps.frost.parsers.SearchParser import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.utils.iab.FrostBilling @@ -127,6 +128,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, onCreateBilling() } + fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) { (0 until tabs.tabCount).asSequence().forEach { i -> action(i, tabs.getTabAt(i)!!.customView as BadgedIcon) @@ -193,7 +195,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, -3L -> launchNewTask(LoginActivity::class.java, clearStack = false) -4L -> launchNewTask(SelectorActivity::class.java, cookies(), false) else -> { - FbCookie.switchUser(profile.identifier, { refreshAll() }) + FbCookie.switchUser(profile.identifier, this@BaseMainActivity::refreshAll) tabsForEachView { _, view -> view.badgeText = null } } } @@ -248,7 +250,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, onClick { _ -> onClick(); false } } - fun refreshAll() { + private fun refreshAll() { fragmentSubject.onNext(REQUEST_REFRESH) } @@ -266,8 +268,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, runOnUiThread { searchView?.results = results } else doAsync { - val data = SearchParser.query(query) ?: return@doAsync - val items = data.map { SearchItem(it.href, it.title, it.description) }.toMutableList() + val data = SearchParser.query(FbCookie.webCookie, query)?.data?.results ?: return@doAsync + val items = data.map(FrostSearch::toSearchItem).toMutableList() if (items.isNotEmpty()) items.add(SearchItem("${FbItem._SEARCH.url}?q=$query", string(R.string.show_all_results), iicon = null)) searchViewCache.put(query, items) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 0d6cce07..e2f7a3d2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -25,10 +25,9 @@ import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL import com.pitchedapps.frost.utils.* import com.pitchedapps.frost.web.LoginWebView -import io.reactivex.Observable +import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.BiFunction -import io.reactivex.internal.operators.single.SingleToObservable import io.reactivex.subjects.SingleSubject @@ -37,18 +36,18 @@ import io.reactivex.subjects.SingleSubject */ class LoginActivity : BaseActivity() { - val toolbar: Toolbar by bindView(R.id.toolbar) - val web: LoginWebView by bindView(R.id.login_webview) - val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) - val textview: AppCompatTextView by bindView(R.id.textview) - val profile: ImageView by bindView(R.id.profile) + private val toolbar: Toolbar by bindView(R.id.toolbar) + private val web: LoginWebView by bindView(R.id.login_webview) + private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh) + private val textview: AppCompatTextView by bindView(R.id.textview) + private val profile: ImageView by bindView(R.id.profile) - val profileObservable = SingleSubject.create<Boolean>() - val usernameObservable = SingleSubject.create<String>() - lateinit var profileLoader: RequestManager + private val profileSubject = SingleSubject.create<Boolean>() + private val usernameSubject = SingleSubject.create<String>() + private lateinit var profileLoader: RequestManager // Helper to set and enable swipeRefresh - var refresh: Boolean + private var refresh: Boolean get() = swipeRefresh.isRefreshing set(value) { if (value) swipeRefresh.isEnabled = true @@ -73,10 +72,12 @@ class LoginActivity : BaseActivity() { profileLoader = Glide.with(profile) } - fun loadInfo(cookie: CookieModel) { + private fun loadInfo(cookie: CookieModel) { refresh = true - Observable.zip(SingleToObservable(profileObservable), SingleToObservable(usernameObservable), - BiFunction<Boolean, String, Pair<Boolean, String>> { foundImage, name -> Pair(foundImage, name) }) + Single.zip<Boolean, String, Pair<Boolean, String>>( + profileSubject, + usernameSubject, + BiFunction(::Pair)) .observeOn(AndroidSchedulers.mainThread()).subscribe { (foundImage, name) -> refresh = false if (!foundImage) { @@ -85,7 +86,11 @@ class LoginActivity : BaseActivity() { } textview.text = String.format(getString(R.string.welcome), name) textview.fadeIn() - frostAnswers { logLogin(LoginEvent().putMethod("frost_browser").putSuccess(true)) } + frostAnswers { + logLogin(LoginEvent() + .putMethod("frost_browser") + .putSuccess(true)) + } /* * The user may have logged into an account that is already in the database * We will let the db handle duplicates and load it now after the new account has been saved @@ -102,23 +107,23 @@ class LoginActivity : BaseActivity() { } - fun loadProfile(id: Long) { + private fun loadProfile(id: Long) { profileLoader.load(PROFILE_PICTURE_URL(id)).withRoundIcon().listener(object : RequestListener<Drawable> { override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean { - profileObservable.onSuccess(true) + profileSubject.onSuccess(true) return false } override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean { e.logFrostAnswers("Profile loading exception") - profileObservable.onSuccess(false) + profileSubject.onSuccess(false) return false } }).into(profile) } - fun loadUsername(cookie: CookieModel) { - cookie.fetchUsername { usernameObservable.onSuccess(it) } + private fun loadUsername(cookie: CookieModel) { + cookie.fetchUsername(usernameSubject::onSuccess) } override fun backConsumer(): Boolean { @@ -129,4 +134,14 @@ class LoginActivity : BaseActivity() { return false } + override fun onResume() { + super.onResume() + web.resumeTimers() + } + + override fun onPause() { + web.pauseTimers() + super.onPause() + } + }
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt index e46a4bfb..14d7ae09 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt @@ -1,5 +1,6 @@ package com.pitchedapps.frost.contracts +import com.pitchedapps.frost.dbflow.CookieModel import io.reactivex.subjects.PublishSubject /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt index 39e8c467..8d625582 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt @@ -13,8 +13,17 @@ package com.pitchedapps.frost.facebook * Matches the fb_dtsg component of a page containing it as a hidden value */ val FB_DTSG_MATCHER: Regex by lazy { Regex("name=\"fb_dtsg\" value=\"(.*?)\"") } +val FB_REV_MATCHER: Regex by lazy{Regex("\"app_version\":\"(.*?)\"")} /** * Matches user id from cookie */ -val FB_USER_MATCHER: Regex by lazy { Regex("c_user=([0-9]*);") }
\ No newline at end of file +val FB_USER_MATCHER: Regex by lazy { Regex("c_user=([0-9]*);") } + +val FB_EPOCH_MATCHER: Regex by lazy { Regex(":([0-9]+)") } +val FB_NOTIF_ID_MATCHER: Regex by lazy { Regex("notif_id\":([0-9]+)") } +val FB_MESSAGE_NOTIF_ID_MATCHER: Regex by lazy { Regex("[thread|user]_fbid_([0-9]+)") } +val FB_CSS_URL_MATCHER: Regex by lazy { Regex("url\\([\"|'](.*?)[\"|']\\)") } + +operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex) + diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt index 428043a0..2fa20917 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRequest.kt @@ -11,7 +11,13 @@ import org.apache.commons.text.StringEscapeUtils /** * Created by Allan Wang on 21/12/17. */ -data class RequestAuth(val userId: Long = -1, val cookie: String = "", val fb_dtsg: String = "") +data class RequestAuth(val userId: Long = -1, + val cookie: String = "", + val fb_dtsg: String = "", + val rev: String = "") { + val isValid + get() = userId > 0 && cookie.isNotEmpty() && fb_dtsg.isNotEmpty() && rev.isNotEmpty() +} private val client: OkHttpClient by lazy { val builder = OkHttpClient.Builder() @@ -21,7 +27,7 @@ private val client: OkHttpClient by lazy { builder.build() } -private fun List<Pair<String, Any?>>.toForm(): RequestBody { +private fun List<Pair<String, Any?>>.toForm(): FormBody { val builder = FormBody.Builder() forEach { (key, value) -> val v = value?.toString() ?: "" @@ -30,6 +36,12 @@ private fun List<Pair<String, Any?>>.toForm(): RequestBody { return builder.build() } +private fun List<Pair<String, Any?>>.withEmptyData(vararg key: String): List<Pair<String, Any?>> { + val newList = toMutableList() + newList.addAll(key.map { it to null }) + return newList +} + private fun String.requestBuilder() = Request.Builder() .header("Cookie", this) .header("User-Agent", USER_AGENT_BASIC) @@ -38,25 +50,33 @@ private fun String.requestBuilder() = Request.Builder() private fun Request.Builder.call() = client.newCall(build()) -fun Pair<Long, String>.getAuth(): RequestAuth? { +fun Pair<Long, String>.getAuth(): RequestAuth { val (userId, cookie) = this + var auth = RequestAuth(userId, cookie) val call = cookie.requestBuilder() - .url(FB_URL_BASE) + .url("https://touch.facebook.com") .get() .call() call.execute().body()?.charStream()?.useLines { it.forEach { val text = StringEscapeUtils.unescapeEcmaScript(it) - val result = FB_DTSG_MATCHER.find(text) - val fb_dtsg = result?.groupValues?.get(1) + val fb_dtsg = FB_DTSG_MATCHER.find(text)[1] if (fb_dtsg != null) { L.d(null, "fb_dtsg for $userId: $fb_dtsg") - return RequestAuth(userId, cookie, fb_dtsg) + auth = auth.copy(fb_dtsg = fb_dtsg) + if (auth.isValid) return auth + } + + val rev = FB_REV_MATCHER.find(text)[1] + if (rev != null) { + L.d(null, "rev for $userId: $rev") + auth = auth.copy(rev = rev) + if (auth.isValid) return auth } } } - return null + return auth } fun RequestAuth.markNotificationRead(notifId: Long): Call { @@ -65,13 +85,9 @@ fun RequestAuth.markNotificationRead(notifId: Long): Call { "click_type" to "notification_click", "id" to notifId, "target_id" to "null", - "m_sess" to null, "fb_dtsg" to fb_dtsg, - "__dyn" to null, - "__req" to null, - "__ajax__" to null, "__user" to userId - ) + ).withEmptyData("m_sess", "__dyn", "__req", "__ajax__") return cookie.requestBuilder() .url("${FB_URL_BASE}a/jewel_notifications_log.php") @@ -96,3 +112,18 @@ fun RequestAuth.markNotificationsRead(vararg notifId: Long) = zip<Long, Boolean, response.body()?.charStream()?.read(buffer) ?: return@zip false !buffer.toString().contains("error") } + +/** + * Execute the call and attempt to check validity + */ +fun Call.executeAndCheck(): Boolean { + val body = execute().body() ?: return false + var empty = true + body.charStream().useLines { + it.forEach { + if (empty && it.isNotEmpty()) empty = false + if (it.contains("error")) return true + } + } + return !empty +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt index 498164c0..58d9ebd4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt @@ -13,7 +13,9 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.contracts.DynamicUiContract import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.contracts.MainActivityContract +import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.enums.FeedSort +import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.parsers.FrostParser import com.pitchedapps.frost.utils.* @@ -28,6 +30,9 @@ import org.jetbrains.anko.toast /** * Created by Allan Wang on 2017-11-07. + * + * All fragments pertaining to the main view + * Must be attached to activities implementing [MainActivityContract] */ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract { @@ -59,6 +64,12 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract { protected abstract val layoutRes: Int + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (context !is MainActivityContract) + throw IllegalArgumentException("${this::class.java.simpleName} is not attached to a context implementing MainActivityContract") + } + override final fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(layoutRes, container, false) val content = view as? FrostContentParent @@ -162,7 +173,7 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract { override fun onTabClick(): Unit = content?.core?.onTabClicked() ?: Unit } -abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), RecyclerContentContract { +abstract class RecyclerFragment<T : Any, Item : IItem<*, *>> : BaseFragment(), RecyclerContentContract { override val layoutRes: Int = R.layout.view_content_recycler @@ -199,7 +210,7 @@ abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), Recycle progress(10) val doc = frostJsoup(baseUrl) progress(60) - val data = parser.parse(doc) + val data = parser.parse(FbCookie.webCookie, doc) if (data == null) { context?.toast(R.string.error_generic) L.eThrow("RecyclerFragment failed for ${baseEnum.name}") @@ -207,7 +218,7 @@ abstract class RecyclerFragment<T, Item : IItem<*, *>> : BaseFragment(), Recycle return@doAsync callback(false) } progress(80) - val items = toItems(data) + val items = toItems(data.data) progress(97) adapter.setNewList(items) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt index 62b1de33..00429730 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt @@ -5,6 +5,7 @@ import com.pitchedapps.frost.contracts.FrostContentContainer import com.pitchedapps.frost.contracts.FrostContentCore import com.pitchedapps.frost.contracts.FrostContentParent import com.pitchedapps.frost.contracts.MainActivityContract +import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.views.FrostRecyclerView import io.reactivex.disposables.Disposable diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt index 186633e5..016f33e8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt @@ -1,6 +1,14 @@ package com.pitchedapps.frost.parsers +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER +import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.facebook.get +import com.pitchedapps.frost.services.NotificationContent +import com.pitchedapps.frost.utils.frostJsoup +import org.jsoup.Jsoup import org.jsoup.nodes.Document +import org.jsoup.nodes.Element /** * Created by Allan Wang on 2017-10-06. @@ -13,80 +21,88 @@ import org.jsoup.nodes.Document * The return type must be nonnull if no parsing errors occurred, as null signifies a parse error * If null really must be allowed, use Optionals */ -interface FrostParser<T> { +interface FrostParser<out T : Any> { + /** - * Extracts data from the JSoup document - * In some cases, the document can be created directly from a connection - * In other times, it needs to be created from scripts, which otherwise - * won't be parsed + * Url to request from */ - fun parse(doc: Document): T? + val url: String /** - * Parse a String input + * Call parsing with default implementation using cookie */ - fun parse(text: String?): T? + fun parse(cookie: String?): ParseResponse<T>? /** - * Take in doc and emit debug output + * Call parsing with given document */ - fun debug(doc: Document): String + fun parse(cookie: String?, document: Document): ParseResponse<T>? /** - * Attempts to parse input and emit a debugger + * Call parsing with given data */ - fun debug(text: String?): String + fun parseFromData(cookie: String?, text: String): ParseResponse<T>? + +} + +data class FrostLink(val text: String, val href: String) + +data class ParseResponse<out T>(val cookie: String, val data: T) { + override fun toString() = "ParseResponse\ncookie: $cookie\ndata:\n$data" } -internal abstract class FrostParserBase<T> : FrostParser<T> { +interface ParseNotification { + fun getUnreadNotifications(data: CookieModel): List<NotificationContent> +} + +internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder().apply { + val tabs = "\t".repeat(indent) + append("$tabs$tag: [\n\t$tabs") + append(this@toJsonString.joinToString("\n\t$tabs")) + append("\n$tabs]\n") +}.toString() + +/** + * T should have a readable toString() function + * [redirectToText] dictates whether all data should be converted to text then back to document before parsing + */ +internal abstract class FrostParserBase<out T : Any>(private val redirectToText: Boolean) : FrostParser<T> { + + override final fun parse(cookie: String?) = parse(cookie, frostJsoup(cookie, url)) - override final fun parse(text: String?): T? { - text ?: return null + override final fun parseFromData(cookie: String?, text: String): ParseResponse<T>? { + cookie ?: return null val doc = textToDoc(text) ?: return null - return parse(doc) + val data = parseImpl(doc) ?: return null + return ParseResponse(cookie, data) } - protected abstract fun textToDoc(text: String): Document? - - override fun debug(text: String?): String { - val result = mutableListOf<String>() - result.add("Testing parser for ${this::class.java.simpleName}") - if (text == null) { - result.add("Null text input") - return result.joinToString("\n") - } - val doc = textToDoc(text) - if (doc == null) { - result.add("Null document from text") - return result.joinToString("\n") - } - return debug(doc, result) + override fun parse(cookie: String?, document: Document): ParseResponse<T>? { + cookie ?: return null + if (redirectToText) + return parseFromData(cookie, document.toString()) + val data = parseImpl(document) ?: return null + return ParseResponse(cookie, data) } - override final fun debug(doc: Document): String { - val result = mutableListOf<String>() - result.add("Testing parser for ${this::class.java.simpleName}") - return debug(doc, result) - } + protected abstract fun parseImpl(doc: Document): T? - private fun debug(doc: Document, result: MutableList<String>): String { - val output = parse(doc) - if (output == null) { - result.add("Output is null") - return result.joinToString("\n") - } else { - result.add("Output is not null") - } - debugImpl(output, result) - return result.joinToString("\n") - } + // protected abstract fun parse(doc: Document): T? - protected abstract fun debugImpl(data: T, result: MutableList<String>) -} + /** + * Attempts to find inner <i> element with some style containing a url + * Returns the formatted url, or an empty string if nothing was found + */ + protected fun Element.getInnerImgStyle() = + FB_CSS_URL_MATCHER.find(select("i.img[style*=url]").attr("style"))[1]?.formattedFbUrl ?: "" -object FrostRegex { - val epoch = Regex(":([0-9]+)") - val notifId = Regex("notif_id\":([0-9]+)") - val messageNotifId = Regex("thread_fbid_([0-9]+)") - val profilePicture = Regex("url\\(\"(.*?)\"\\)") + protected open fun textToDoc(text: String) = if (!redirectToText) + Jsoup.parse(text) + else + throw RuntimeException("${this::class.java.simpleName} requires text redirect but did not implement textToDoc") + + protected fun parseLink(element: Element?): FrostLink? { + val a = element?.getElementsByTag("a")?.first() ?: return null + return FrostLink(a.text(), a.attr("href")) + } }
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt index 9430407d..9d4a2193 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt @@ -1,6 +1,8 @@ package com.pitchedapps.frost.parsers -import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.services.NotificationContent import com.pitchedapps.frost.utils.L import org.apache.commons.text.StringEscapeUtils import org.jsoup.Jsoup @@ -14,13 +16,55 @@ import org.jsoup.nodes.Element * We can parse out the content we want directly and load it ourselves * */ -object MessageParser : FrostParser<Triple<List<FrostThread>, FrostLink?, List<FrostLink>>> by MessageParserImpl() +object MessageParser : FrostParser<FrostMessages> by MessageParserImpl() -data class FrostThread(val id: Int, val img: String, val title: String, val time: Long, val url: String, val unread: Boolean, val content: String?) +data class FrostMessages(val threads: List<FrostThread>, + val seeMore: FrostLink?, + val extraLinks: List<FrostLink> +) : ParseNotification { + override fun toString() = StringBuilder().apply { + append("FrostMessages {\n") + append(threads.toJsonString("threads", 1)) + append("\tsee more: $seeMore\n") + append(extraLinks.toJsonString("extra links", 1)) + append("}") + }.toString() -data class FrostLink(val text: String, val href: String) + override fun getUnreadNotifications(data: CookieModel) = + threads.filter(FrostThread::unread).map { + with(it) { + NotificationContent( + data = data, + notifId = Math.abs(id.toInt()), + href = url, + title = title, + text = content ?: "", + timestamp = time, + profileUrl = img + ) + } + } +} + +/** + * [id] user/thread id, or current time fallback + * [img] parsed url for profile img + * [time] time of message + * [url] link to thread + * [unread] true if image is unread, false otherwise + * [content] optional string for thread + */ +data class FrostThread(val id: Long, + val img: String, + val title: String, + val time: Long, + val url: String, + val unread: Boolean, + val content: String?) -private class MessageParserImpl : FrostParserBase<Triple<List<FrostThread>, FrostLink?, List<FrostLink>>>() { +private class MessageParserImpl : FrostParserBase<FrostMessages>(true) { + + override val url = FbItem.MESSAGES.url override fun textToDoc(text: String): Document? { var content = StringEscapeUtils.unescapeEcmaScript(text) @@ -39,32 +83,29 @@ private class MessageParserImpl : FrostParserBase<Triple<List<FrostThread>, Fros return Jsoup.parseBodyFragment("<div $content") } - override fun parse(doc: Document): Triple<List<FrostThread>, FrostLink?, List<FrostLink>>? { - val threadList = doc.getElementById("threadlist_rows") + override fun parseImpl(doc: Document): FrostMessages? { + val threadList = doc.getElementById("threadlist_rows") ?: return null val threads: List<FrostThread> = threadList.getElementsByAttributeValueContaining("id", "thread_fbid_") - .mapNotNull { parseMessage(it) } + .mapNotNull(this::parseMessage) val seeMore = parseLink(doc.getElementById("see_older_threads")) val extraLinks = threadList.nextElementSibling().select("a") - .mapNotNull { parseLink(it) } - return Triple(threads, seeMore, extraLinks) + .mapNotNull(this::parseLink) + return FrostMessages(threads, seeMore, extraLinks) } private fun parseMessage(element: Element): FrostThread? { val a = element.getElementsByTag("a").first() ?: return null val abbr = element.getElementsByTag("abbr") - val epoch = FrostRegex.epoch.find(abbr.attr("data-store")) - ?.groupValues?.getOrNull(1)?.toLongOrNull() ?: -1L + val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L //fetch id - val id = FrostRegex.messageNotifId.find(element.id()) - ?.groupValues?.getOrNull(1)?.toLongOrNull() ?: System.currentTimeMillis() + val id = FB_MESSAGE_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull() + ?: System.currentTimeMillis() val content = element.select("span.snippet").firstOrNull()?.text()?.trim() - //fetch convo pic - val p = element.select("i.img[style*=url]") - val pUrl = FrostRegex.profilePicture.find(p.attr("style"))?.groups?.get(1)?.value?.formattedFbUrl ?: "" + val img = element.getInnerImgStyle() L.v("url", a.attr("href")) return FrostThread( - id = id.toInt(), - img = pUrl.formattedFbUrl, + id = id, + img = img, title = a.text(), time = epoch, url = a.attr("href").formattedFbUrl, @@ -73,15 +114,4 @@ private class MessageParserImpl : FrostParserBase<Triple<List<FrostThread>, Fros ) } - private fun parseLink(element: Element?): FrostLink? { - val a = element?.getElementsByTag("a")?.first() ?: return null - return FrostLink(a.text(), a.attr("href")) - } - - override fun debugImpl(data: Triple<List<FrostThread>, FrostLink?, List<FrostLink>>, result: MutableList<String>) { - result.addAll(data.first.map(FrostThread::toString)) - result.add("See more link:") - result.add("\t${data.second}") - result.addAll(data.third.map(FrostLink::toString)) - } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt new file mode 100644 index 00000000..f743a43a --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt @@ -0,0 +1,92 @@ +package com.pitchedapps.frost.parsers + +import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.services.NotificationContent +import com.pitchedapps.frost.utils.L +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +/** + * Created by Allan Wang on 2017-12-25. + * + */ +object NotifParser : FrostParser<FrostNotifs> by NotifParserImpl() + +data class FrostNotifs( + val notifs: List<FrostNotif>, + val seeMore: FrostLink? +) : ParseNotification { + override fun toString() = StringBuilder().apply { + append("FrostNotifs {\n") + append(notifs.toJsonString("notifs", 1)) + append("\tsee more: $seeMore\n") + append("}") + }.toString() + + override fun getUnreadNotifications(data: CookieModel) = + notifs.filter(FrostNotif::unread).map { + with(it) { + NotificationContent( + data = data, + notifId = Math.abs(id.toInt()), + href = url, + title = null, + text = content ?: "", + timestamp = time, + profileUrl = img + ) + } + } +} + +/** + * [id] notif id, or current time fallback + * [img] parsed url for profile img + * [time] time of message + * [url] link to thread + * [unread] true if image is unread, false otherwise + * [content] optional string for thread + */ +data class FrostNotif(val id: Long, + val img: String, + val time: Long, + val url: String, + val unread: Boolean, + val content: String?) + +private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) { + + override val url = FbItem.NOTIFICATIONS.url + + override fun parseImpl(doc: Document): FrostNotifs? { + val notificationList = doc.getElementById("notifications_list") ?: return null + val notifications = notificationList.getElementsByAttributeValueContaining("id", "list_notif_") + .mapNotNull { parseNotif(it) } + val seeMore = parseLink(doc.getElementsByAttributeValue("href", "/notifications.php?more").first()) + return FrostNotifs(notifications, seeMore) + } + + private fun parseNotif(element: Element): FrostNotif? { + val a = element.getElementsByTag("a").first() ?: return null + val abbr = element.getElementsByTag("abbr") + val epoch = FB_EPOCH_MATCHER.find(abbr.attr("data-store"))[1]?.toLongOrNull() ?: -1L + //fetch id + val id = FB_NOTIF_ID_MATCHER.find(element.id())[1]?.toLongOrNull() + ?: System.currentTimeMillis() + val img = element.getInnerImgStyle() + val timeString = abbr.text() + val content = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() //remove + L.v("url", a.attr("href")) + return FrostNotif( + id = id, + img = img, + time = epoch, + url = a.attr("href").formattedFbUrl, + unread = !element.hasClass("acw"), + content = content + ) + } + + +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt index 908bb153..bc09d4db 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt @@ -1,8 +1,10 @@ package com.pitchedapps.frost.parsers -import ca.allanwang.kau.utils.withMaxLength +import ca.allanwang.kau.searchview.SearchItem +import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.parsers.FrostSearch.Companion.create import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.frostJsoup import org.jsoup.Jsoup @@ -12,11 +14,11 @@ import org.jsoup.nodes.Element /** * Created by Allan Wang on 2017-10-09. */ -object SearchParser : FrostParser<List<FrostSearch>> by SearchParserImpl() { - fun query(input: String): List<FrostSearch>? { +object SearchParser : FrostParser<FrostSearches> by SearchParserImpl() { + fun query(cookie: String?, input: String): ParseResponse<FrostSearches>? { val url = "${FbItem._SEARCH.url}?q=${if (input.isNotBlank()) input else "a"}" L.i(null, "Search Query $url") - return parse(frostJsoup(url)) + return parse(cookie, frostJsoup(url)) } } @@ -25,25 +27,40 @@ enum class SearchKeys(val key: String) { EVENTS("keywords_events") } +data class FrostSearches(val results: List<FrostSearch>) { + + override fun toString() = StringBuilder().apply { + append("FrostSearches {\n") + append(results.toJsonString("results", 1)) + append("}") + }.toString() +} + /** * As far as I'm aware, all links are independent, so the queries don't matter * A lot of it is tracking information, which I'll strip away * Other text items are formatted for safety + * + * Note that it's best to create search results from [create] */ -class FrostSearch(href: String, title: String, description: String?) { - val href = with(href.indexOf("?")) { if (this == -1) href else href.substring(0, this) } - val title = title.format() - val description = description?.format() +data class FrostSearch(val href: String, val title: String, val description: String?) { - private fun String.format() = replace("\n", " ").withMaxLength(50) - - override fun toString(): String - = "FrostSearch(href=$href, title=$title, description=$description)" + fun toSearchItem() = SearchItem(href, title, description) + companion object { + fun create(href: String, title: String, description: String?) = FrostSearch( + with(href.indexOf("?")) { if (this == -1) href else href.substring(0, this) }, + title.format(), + description?.format() + ) + } } -private class SearchParserImpl : FrostParserBase<List<FrostSearch>>() { - override fun parse(doc: Document): List<FrostSearch>? { +private class SearchParserImpl : FrostParserBase<FrostSearches>(false) { + + override val url = "${FbItem._SEARCH.url}?q=a" + + override fun parseImpl(doc: Document): FrostSearches? { val container: Element = doc.getElementById("BrowseResultsContainer") ?: doc.getElementById("root") ?: return null @@ -51,19 +68,11 @@ private class SearchParserImpl : FrostParserBase<List<FrostSearch>>() { * * Removed [data-store*=result_id] */ - return container.select("a.touchable[href]").filter(Element::hasText).map { - FrostSearch(it.attr("href").formattedFbUrl, + return FrostSearches(container.select("a.touchable[href]").filter(Element::hasText).map { + FrostSearch.create(it.attr("href").formattedFbUrl, it.select("._uoi").first()?.text() ?: "", it.select("._1tcc").first()?.text()) - }.filter { it.title.isNotBlank() } - } - - - override fun textToDoc(text: String): Document? = Jsoup.parse(text) - - override fun debugImpl(data: List<FrostSearch>, result: MutableList<String>) { - result.add("Has size ${data.size}") - result.addAll(data.map(FrostSearch::toString)) + }.filter { it.title.isNotBlank() }) } }
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 44b01bc3..afa30a91 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -24,12 +24,17 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.FrostWebActivity import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.dbflow.NotificationModel +import com.pitchedapps.frost.dbflow.lastNotificationTime import com.pitchedapps.frost.enums.OverlayContext import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.formattedFbUrl -import com.pitchedapps.frost.parsers.FrostThread +import com.pitchedapps.frost.parsers.FrostParser +import com.pitchedapps.frost.parsers.MessageParser +import com.pitchedapps.frost.parsers.NotifParser +import com.pitchedapps.frost.parsers.ParseNotification import com.pitchedapps.frost.utils.* import org.jetbrains.anko.runOnUiThread +import java.util.* /** * Created by Allan Wang on 2017-07-08. @@ -88,23 +93,66 @@ class FrostNotificationTarget(val context: Context, * Enum to handle notification creations */ enum class NotificationType( - private val groupPrefix: String, private val overlayContext: OverlayContext, - private val contentRes: Int, - private val pendingUrl: String, + private val fbItem: FbItem, + private val parser: FrostParser<ParseNotification>, + private val getTime: (notif: NotificationModel) -> Long, + private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel, private val ringtone: () -> String) { - GENERAL("frost", OverlayContext.NOTIFICATION, R.string.notifications, FbItem.NOTIFICATIONS.url, { Prefs.notificationRingtone }), - MESSAGE("frost_im", OverlayContext.MESSAGE, R.string.messages, FbItem.MESSAGES.url, { Prefs.messageRingtone }); + GENERAL(OverlayContext.NOTIFICATION, + FbItem.NOTIFICATIONS, + NotifParser, + NotificationModel::epoch, + { notif, time -> notif.copy(epoch = time) }, + Prefs::notificationRingtone), + MESSAGE(OverlayContext.MESSAGE, + FbItem.MESSAGES, + MessageParser, + NotificationModel::epochIm, + { notif, time -> notif.copy(epochIm = time) }, + Prefs::messageRingtone); + + private val groupPrefix = "frost_${name.toLowerCase(Locale.CANADA)}" + + /** + * Get unread data from designated parser + * Display notifications for those after old epoch + * Save new epoch + */ + fun fetch(context: Context, data: CookieModel) { + val response = parser.parse(data.cookie) + ?: return L.eThrow("$name notification data not found") + val notifs = response.data.getUnreadNotifications(data) + if (notifs.isEmpty()) return + var notifCount = 0 + val userId = data.id + val prevNotifTime = lastNotificationTime(userId) + val prevLatestEpoch = getTime(prevNotifTime) + L.v("Notif $name prev epoch $prevLatestEpoch") + var newLatestEpoch = prevLatestEpoch + notifs.forEach { notif -> + L.v("Notif timestamp ${notif.timestamp}") + if (notif.timestamp <= prevLatestEpoch) return@forEach + createNotification(context, notif, notifCount == 0) + if (notif.timestamp > newLatestEpoch) + newLatestEpoch = notif.timestamp + notifCount++ + } + if (newLatestEpoch != prevLatestEpoch) + putTime(prevNotifTime, newLatestEpoch).save() + L.d("Notif $name new epoch ${getTime(lastNotificationTime(userId))}") + summaryNotification(context, userId, notifCount) + } /** * Create and submit a new notification with the given [content] * If [withDefaults] is set, it will also add the appropriate sound, vibration, and light * Note that when we have multiple notifications coming in at once, we don't want to have defaults for all of them */ - fun createNotification(context: Context, content: NotificationContent, withDefaults: Boolean) { + private fun createNotification(context: Context, content: NotificationContent, withDefaults: Boolean) { with(content) { val intent = Intent(context, FrostWebActivity::class.java) - intent.data = Uri.parse(href.formattedFbUrl) + intent.data = Uri.parse(href) intent.putExtra(ARG_USER_ID, data.id) intent.putExtra(ARG_OVERLAY_CONTEXT, overlayContext) val group = "${groupPrefix}_${data.id}" @@ -142,16 +190,16 @@ enum class NotificationType( * This will always produce sound, vibration, and lights based on preferences * and will only show if we have at least 2 notifications */ - fun summaryNotification(context: Context, userId: Long, count: Int) { + private fun summaryNotification(context: Context, userId: Long, count: Int) { frostAnswersCustom("Notifications", "Type" to name, "Count" to count) if (count <= 1) return val intent = Intent(context, FrostWebActivity::class.java) - intent.data = Uri.parse(pendingUrl) + intent.data = Uri.parse(fbItem.url) intent.putExtra(ARG_USER_ID, userId) val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) val notifBuilder = context.frostNotification.withDefaults(ringtone()) .setContentTitle(context.string(R.string.frost_name)) - .setContentText("$count ${context.string(contentRes)}") + .setContentText("$count ${context.string(fbItem.titleId)}") .setGroup("${groupPrefix}_$userId") .setGroupSummary(true) .setContentIntent(pendingIntent) @@ -167,13 +215,10 @@ enum class NotificationType( data class NotificationContent(val data: CookieModel, val notifId: Int, val href: String, - val title: String? = null, + val title: String? = null, // defaults to frost title val text: String, val timestamp: Long, - val profileUrl: String) { - constructor(data: CookieModel, thread: FrostThread) - : this(data, thread.id, thread.url, thread.title, thread.content ?: "", thread.time, thread.img) -} + val profileUrl: String) const val NOTIFICATION_PERIODIC_JOB = 7 diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index c4ab6161..adeefec6 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -7,18 +7,11 @@ import android.support.v4.app.NotificationManagerCompat import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R -import com.pitchedapps.frost.dbflow.CookieModel -import com.pitchedapps.frost.dbflow.lastNotificationTime import com.pitchedapps.frost.dbflow.loadFbCookiesSync -import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.formattedFbUrl -import com.pitchedapps.frost.parsers.MessageParser import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostAnswersCustom -import com.pitchedapps.frost.utils.frostJsoup import org.jetbrains.anko.doAsync -import org.jsoup.nodes.Element import java.util.concurrent.Future /** @@ -27,8 +20,7 @@ import java.util.concurrent.Future * Service to manage notifications * Will periodically check through all accounts in the db and send notifications when appropriate * - * Note that general notifications are parsed directly with Jsoup, - * but instant messages are done so with a headless webview as it is generated from JS + * All fetching is done through parsers */ class NotificationService : JobService() { @@ -36,13 +28,6 @@ class NotificationService : JobService() { val startTime = System.currentTimeMillis() - companion object { - val epochMatcher: Regex by lazy { Regex(":([0-9]*?),") } - val notifIdMatcher: Regex by lazy { Regex("notif_id\":([0-9]*?),") } - val messageNotifIdMatcher: Regex by lazy { Regex("thread_fbid_([0-9]+)") } - val profMatcher: Regex by lazy { Regex("url\\(\"(.*?)\"\\)") } - } - override fun onStopJob(params: JobParameters?): Boolean { val time = System.currentTimeMillis() - startTime L.d("Notification service has finished abruptly in $time ms") @@ -70,104 +55,28 @@ class NotificationService : JobService() { override fun onStartJob(params: JobParameters?): Boolean { L.i("Fetching notifications") future = doAsync { + val context = weakRef.get() + ?: return@doAsync L.eThrow("NotificationService had null weakRef to self") val currentId = Prefs.userId val cookies = loadFbCookiesSync() cookies.forEach { val current = it.id == currentId if (current || Prefs.notificationAllAccounts) - fetchGeneralNotifications(it) - if (Prefs.notificationsInstantMessages && (current || Prefs.notificationsImAllAccounts)) - fetchMessageNotifications(it) + NotificationType.GENERAL.fetch(context, it) + if (Prefs.notificationsInstantMessages + && (current || Prefs.notificationsImAllAccounts)) + NotificationType.MESSAGE.fetch(context, it) } finish(params) } return true } - fun logNotif(text: String): NotificationContent? { + private fun logNotif(text: String): NotificationContent? { L.eThrow("NotificationService: $text") return null } - /* - * ---------------------------------------------------------------- - * General notification logic. - * Fetch notifications -> Filter new ones -> Parse notifications -> - * Show notifications -> Show group notification - * ---------------------------------------------------------------- - */ - - fun fetchGeneralNotifications(data: CookieModel) { - L.d("Notif fetch", data.toString()) - val doc = frostJsoup(data.cookie, FbItem.NOTIFICATIONS.url) - //aclb for unread, acw for read - val unreadNotifications = (doc.getElementById("notifications_list") ?: return L.eThrow("Notification list not found")).getElementsByClass("aclb") - var notifCount = 0 - //val prevLatestEpoch = 1498931565L // for testing - val prevNotifTime = lastNotificationTime(data.id) - val prevLatestEpoch = prevNotifTime.epoch - L.v("Notif Prev Latest Epoch $prevLatestEpoch") - var newLatestEpoch = prevLatestEpoch - unreadNotifications.forEach unread@ { elem -> - val notif = parseNotification(data, elem) ?: return@unread - L.v("Notif timestamp ${notif.timestamp}") - if (notif.timestamp <= prevLatestEpoch) return@unread - NotificationType.GENERAL.createNotification(this, notif, notifCount == 0) - if (notif.timestamp > newLatestEpoch) - newLatestEpoch = notif.timestamp - notifCount++ - } - if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epoch = newLatestEpoch).save() - L.d("Notif new latest epoch ${lastNotificationTime(data.id).epoch}") - NotificationType.GENERAL.summaryNotification(this, data.id, notifCount) - } - - fun parseNotification(data: CookieModel, element: Element): NotificationContent? { - val a = element.getElementsByTag("a").first() ?: return logNotif("IM No a tag") - val abbr = element.getElementsByTag("abbr") - val epoch = epochMatcher.find(abbr.attr("data-store"))?.groups?.get(1)?.value?.toLong() ?: return logNotif("IM No epoch") - //fetch id - val notifId = notifIdMatcher.find(a.attr("data-store"))?.groups?.get(1)?.value?.toLong() ?: System.currentTimeMillis() - val timeString = abbr.text() - val text = a.text().replace("\u00a0", " ").removeSuffix(timeString).trim() //remove - if (Prefs.notificationKeywords.any { text.contains(it, ignoreCase = true) }) return null //notification filtered out - //fetch profpic - val p = element.select("i.img[style*=url]") - val pUrl = profMatcher.find(p.attr("style"))?.groups?.get(1)?.value?.formattedFbUrl ?: "" - return NotificationContent(data, notifId.toInt(), a.attr("href"), null, text, epoch, pUrl) - } - - /* - * ---------------------------------------------------------------- - * Instant message notification logic. - * Fetch notifications -> Filter new ones -> Parse notifications -> - * Show notifications -> Show group notification - * ---------------------------------------------------------------- - */ - - fun fetchMessageNotifications(data: CookieModel) { - L.d("Notif IM fetch", data.toString()) - val doc = frostJsoup(data.cookie, FbItem.MESSAGES.url) - val (threads, _, _) = MessageParser.parse(doc.toString()) ?: return L.e("Could not parse IM") - - var notifCount = 0 - val prevNotifTime = lastNotificationTime(data.id) - val prevLatestEpoch = prevNotifTime.epochIm - L.v("Notif Prev Latest Im Epoch $prevLatestEpoch") - var newLatestEpoch = prevLatestEpoch - threads.filter { it.unread }.forEach { notif -> - L.v("Notif Im timestamp ${notif.time}") - if (notif.time <= prevLatestEpoch) return@forEach - NotificationType.MESSAGE.createNotification(this, NotificationContent(data, notif), notifCount == 0) - if (notif.time > newLatestEpoch) - newLatestEpoch = notif.time - notifCount++ - } - if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epochIm = newLatestEpoch).save() - L.d("Notif new latest im epoch ${lastNotificationTime(data.id).epochIm}") - NotificationType.MESSAGE.summaryNotification(this, data.id, notifCount) - } - private fun Context.debugNotification(text: String) { if (!BuildConfig.DEBUG) return val notifBuilder = frostNotification.withDefaults() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index 4bd41802..1108f5d4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -22,7 +22,7 @@ import com.pitchedapps.frost.views.Keywords */ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { - text(R.string.notification_frequency, { Prefs.notificationFreq }, { Prefs.notificationFreq = it }) { + text(R.string.notification_frequency, Prefs::notificationFreq, { Prefs.notificationFreq = it }) { val options = longArrayOf(-1, 15, 30, 60, 120, 180, 300, 1440, 2880) val texts = options.map { if (it <= 0) string(R.string.no_notifications) else minuteToText(it) } onClick = { @@ -52,23 +52,27 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { } } - checkbox(R.string.notification_all_accounts, { Prefs.notificationAllAccounts }, { Prefs.notificationAllAccounts = it }) { + checkbox(R.string.notification_all_accounts, Prefs::notificationAllAccounts, { Prefs.notificationAllAccounts = it }) { descRes = R.string.notification_all_accounts_desc } - checkbox(R.string.notification_messages, { Prefs.notificationsInstantMessages }, { Prefs.notificationsInstantMessages = it; reloadByTitle(R.string.notification_messages_all_accounts) }) { + checkbox(R.string.notification_messages, Prefs::notificationsInstantMessages, { Prefs.notificationsInstantMessages = it; reloadByTitle(R.string.notification_messages_all_accounts) }) { descRes = R.string.notification_messages_desc } - checkbox(R.string.notification_messages_all_accounts, { Prefs.notificationsImAllAccounts }, { Prefs.notificationsImAllAccounts = it }) { + checkbox(R.string.notification_messages_all_accounts, Prefs::notificationsImAllAccounts, { Prefs.notificationsImAllAccounts = it }) { descRes = R.string.notification_messages_all_accounts_desc - enabler = { Prefs.notificationsInstantMessages } + enabler = Prefs::notificationsInstantMessages } - checkbox(R.string.notification_sound, { Prefs.notificationSound }, { Prefs.notificationSound = it; reloadByTitle(R.string.notification_ringtone, R.string.message_ringtone) }) + checkbox(R.string.notification_sound, Prefs::notificationSound, { + Prefs.notificationSound = it + reloadByTitle(R.string.notification_ringtone, + R.string.message_ringtone) + }) fun KPrefText.KPrefTextContract<String>.ringtone(code: Int) { - enabler = { Prefs.notificationSound } + enabler = Prefs::notificationSound textGetter = { if (it.isBlank()) string(R.string.kau_default) else RingtoneManager.getRingtone(this@getNotificationPrefs, Uri.parse(it)) @@ -87,17 +91,17 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = { } } - text(R.string.notification_ringtone, { Prefs.notificationRingtone }, { Prefs.notificationRingtone = it }) { + text(R.string.notification_ringtone, Prefs::notificationRingtone, { Prefs.notificationRingtone = it }) { ringtone(SettingsActivity.REQUEST_NOTIFICATION_RINGTONE) } - text(R.string.message_ringtone, { Prefs.messageRingtone }, { Prefs.messageRingtone = it }) { + text(R.string.message_ringtone, Prefs::messageRingtone, { Prefs.messageRingtone = it }) { ringtone(SettingsActivity.REQUEST_MESSAGE_RINGTONE) } - checkbox(R.string.notification_vibrate, { Prefs.notificationVibrate }, { Prefs.notificationVibrate = it }) + checkbox(R.string.notification_vibrate, Prefs::notificationVibrate, { Prefs.notificationVibrate = it }) - checkbox(R.string.notification_lights, { Prefs.notificationLights }, { Prefs.notificationLights = it }) + checkbox(R.string.notification_lights, Prefs::notificationLights, { Prefs.notificationLights = it }) plainText(R.string.notification_fetch_now) { descRes = R.string.notification_fetch_now_desc diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt index f8b487a2..9251e607 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt @@ -2,6 +2,7 @@ package com.pitchedapps.frost.web import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.graphics.Color import android.util.AttributeSet import android.view.View @@ -12,6 +13,7 @@ import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FB_LOGIN_URL import com.pitchedapps.frost.facebook.FB_USER_MATCHER import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.get import com.pitchedapps.frost.injectors.CssHider import com.pitchedapps.frost.injectors.jsInject import com.pitchedapps.frost.utils.L @@ -31,7 +33,7 @@ class LoginWebView @JvmOverloads constructor( private lateinit var progressCallback: (Int) -> Unit init { - FbCookie.reset { setupWebview() } + FbCookie.reset(this::setupWebview) } @SuppressLint("SetJavaScriptEnabled") @@ -62,13 +64,14 @@ class LoginWebView @JvmOverloads constructor( if (!url.isFacebookUrl) return@doAsync val cookie = CookieManager.getInstance().getCookie(url) ?: return@doAsync L.d("Checking cookie for login", cookie) - val id = FB_USER_MATCHER.find(cookie)?.groupValues?.get(1)?.toLong() ?: return@doAsync + val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return@doAsync uiThread { onFound(id, cookie) } } } override fun onPageCommitVisible(view: WebView, url: String?) { super.onPageCommitVisible(view, url) + L.d("Login page commit visible") view.setBackgroundColor(Color.TRANSPARENT) if (url.isFacebookUrl) view.jsInject(CssHider.HEADER, diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index b5e91310..01785447 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -6,6 +6,13 @@ <item text="" /> --> + <version title="v1.7.2" /> + <item text="Optimize login view" /> + <item text="Rewrite parsers" /> + <item text="Fix message notification icons" /> + <item text="Small theme updates" /> + <item text="" /> + <item text="" /> <version title="v1.7.1" /> <item text="Fix launching messages in new overlay" /> @@ -14,8 +21,6 @@ <item text="Automatically bring toolbar up when keyboard is shown" /> <item text="Rewrite theme components to fully support AMOLED and improve light" /> <item text="Properly pause webviews when not in use" /> - <item text="" /> - <item text="" /> <version title="v1.7.0" /> <item text="Fully customize your tabs! Check out settings > appearance > main activity tabs" /> diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbParseTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbParseTest.kt new file mode 100644 index 00000000..65777f97 --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbParseTest.kt @@ -0,0 +1,46 @@ +package com.pitchedapps.frost.facebook + +import com.pitchedapps.frost.internal.COOKIE +import com.pitchedapps.frost.internal.assertComponentsNotEmpty +import com.pitchedapps.frost.internal.assertDescending +import com.pitchedapps.frost.internal.authDependent +import com.pitchedapps.frost.parsers.* +import org.junit.BeforeClass +import org.junit.Test +import kotlin.test.fail + +/** + * Created by Allan Wang on 24/12/17. + */ +class FbParseTest { + + companion object { + @BeforeClass + @JvmStatic + fun before() { + authDependent() + } + } + + private inline fun <T : Any> FrostParser<T>.test(action: T.() -> Unit = {}) { + val response = parse(COOKIE) + ?: fail("${this::class.java.simpleName} returned null for $url") + println(response) + response.data.action() + } + + @Test + fun message() = MessageParser.test { + threads.forEach(FrostThread::assertComponentsNotEmpty) + threads.map(FrostThread::time).assertDescending("thread time values") + } + + @Test + fun search() = SearchParser.test() + + @Test + fun notif() = NotifParser.test { + notifs.forEach(FrostNotif::assertComponentsNotEmpty) + notifs.map(FrostNotif::time).assertDescending("notif time values") + } +}
\ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt new file mode 100644 index 00000000..a21bcb13 --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt @@ -0,0 +1,41 @@ +package com.pitchedapps.frost.facebook + +import org.apache.commons.text.StringEscapeUtils +import org.junit.Test +import kotlin.test.assertEquals + +/** + * Created by Allan Wang on 24/12/17. + */ +class FbRegexTest { + @Test + fun userIdRegex() { + val id = 12349876L + val cookie = "wd=1366x615; c_user=$id; act=1234%2F12; m_pixel_ratio=1; presence=hello; x-referer=asdfasdf" + assertEquals(id, FB_USER_MATCHER.find(cookie)[1]?.toLong()) + } + + @Test + fun fbDtsgRegex() { + val fb_dtsg = "readme" + val input = "data-sigil=\"mbasic_inline_feed_composer\">\u003Cinput type=\"hidden\" name=\"fb_dtsg\" value=\"$fb_dtsg\" autocomplete=\"off\" \\/>\u003Cinput type=\"hidden\" name=\"privacyx\" value=\"12345\"" + assertEquals(fb_dtsg, FB_DTSG_MATCHER.find(input)[1]) + } + + @Test + fun ppRegex() { + val img = "https\\3a //scontent-yyz1-1.xx.fbcdn.net/v/asdf1234.jpg?efg\\3d 333\\26 oh\\3d 77\\26 oe\\3d 444" + val ppStyle = "background:#d8dce6 url('$img') no-repeat center;background-size:100% 100%;-webkit-background-size:100% 100%;width:58px;height:58px;" + assertEquals(StringEscapeUtils.unescapeCsv(img), StringEscapeUtils.unescapeCsv(FB_CSS_URL_MATCHER.find(ppStyle)[1])) + } + + @Test + fun msgNotifIdRegex() { + val id = 1273491646093428L + val data = "threadlist_row_other_user_fbid_thread_fbid_$id" + assertEquals(id, FB_MESSAGE_NOTIF_ID_MATCHER.find(data)[1]?.toLong(), "thread_fbid mismatch") + val userData = "threadlist_row_other_user_fbid_${id}thread_fbid_" + assertEquals(id, FB_MESSAGE_NOTIF_ID_MATCHER.find(userData)[1]?.toLong(), "user_fbid mismatch") + + } +}
\ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRequestTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRequestTest.kt index a521ceda..16894b16 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRequestTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRequestTest.kt @@ -1,14 +1,13 @@ package com.pitchedapps.frost.facebook +import com.pitchedapps.frost.internal.AUTH import com.pitchedapps.frost.internal.COOKIE -import com.pitchedapps.frost.internal.FB_DTSG import com.pitchedapps.frost.internal.USER_ID -import org.junit.Assume +import com.pitchedapps.frost.internal.authDependent +import okhttp3.Call import org.junit.BeforeClass import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull +import kotlin.test.* /** * Created by Allan Wang on 21/12/17. @@ -19,44 +18,34 @@ class FbRequestTest { @BeforeClass @JvmStatic fun before() { - Assume.assumeTrue(COOKIE.isNotEmpty()) + authDependent() } - - val AUTH: RequestAuth by lazy { RequestAuth(USER_ID, COOKIE, FB_DTSG) } } - @Test - fun userIdRegex() { - val id = 12349876L - val cookie = "wd=1366x615; c_user=$id; act=1234%2F12; m_pixel_ratio=1; presence=hello; x-referer=asdfasdf" - assertEquals(id, FB_USER_MATCHER.find(cookie)?.groupValues?.get(1)?.toLong()) - } - - @Test - fun fbDtsgRegex() { - val fb_dtsg = "readme" - val input = "data-sigil=\"mbasic_inline_feed_composer\">\u003Cinput type=\"hidden\" name=\"fb_dtsg\" value=\"$fb_dtsg\" autocomplete=\"off\" \\/>\u003Cinput type=\"hidden\" name=\"privacyx\" value=\"12345\"" - assertEquals(fb_dtsg, FB_DTSG_MATCHER.find(input)?.groupValues?.get(1)) + /** + * Used to emulate [executeAndCheck] + * Must be consistent with that method + */ + private fun Call.assertNoError() { + val data = execute().body()?.string() ?: fail("Content was null") + println("Call response: $data") + assertTrue(data.isNotEmpty(), "Content was empty") + assertFalse(data.contains("error"), "Content had error") } @Test fun auth() { val auth = (USER_ID to COOKIE).getAuth() assertNotNull(auth) - assertEquals(USER_ID, auth!!.userId) + assertEquals(USER_ID, auth.userId) assertEquals(COOKIE, auth.cookie) - println("Test auth: priv $FB_DTSG, test ${auth.fb_dtsg}") + println("Test auth: ${auth.fb_dtsg}") } @Test fun markNotification() { val notifId = 1513544657695779 - - val out = AUTH.markNotificationRead(notifId) - .execute().body()?.string() ?: "" - println(out) - - assertFalse(out.contains("error")) + AUTH.markNotificationRead(notifId).assertNoError() } }
\ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt b/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt index 91eb968d..deaed333 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/internal/Internal.kt @@ -1,9 +1,15 @@ package com.pitchedapps.frost.internal -import com.pitchedapps.frost.facebook.FB_USER_MATCHER +import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.utils.frostJsoup +import org.junit.Assume import java.io.File import java.io.FileInputStream import java.util.* +import kotlin.reflect.full.starProjectedType +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail /** * Created by Allan Wang on 21/12/17. @@ -24,5 +30,46 @@ val PROPS: Properties by lazy { } val COOKIE: String by lazy { PROPS.getProperty("COOKIE") ?: "" } -val FB_DTSG: String by lazy { PROPS.getProperty("FB_DTSG") ?: "" } -val USER_ID: Long by lazy { FB_USER_MATCHER.find(COOKIE)?.groupValues?.get(1)?.toLong() ?: -1 } +val USER_ID: Long by lazy { FB_USER_MATCHER.find(COOKIE)[1]?.toLong() ?: -1 } +val AUTH: RequestAuth by lazy { + (USER_ID to COOKIE).getAuth().apply { + println("Auth:\nuser:$userId\nfb_dtsg: $fb_dtsg\nrev: $rev\nvalid: $isValid") + } +} + +val VALID_COOKIE: Boolean by lazy { + val data = testJsoup(FbItem.SETTINGS.url) + data.title() == "Settings" +} + +fun testJsoup(url: String) = frostJsoup(COOKIE, url) + +fun authDependent() { + println("Auth Dependent") + Assume.assumeTrue(COOKIE.isNotEmpty() && VALID_COOKIE) + Assume.assumeTrue(AUTH.isValid) +} + +/** + * Check that component strings are nonempty and are properly parsed + * To be used for data classes + */ +fun Any.assertComponentsNotEmpty() { + val components = this::class.members.filter { it.name.startsWith("component") } + if (components.isEmpty()) + fail("${this::class.simpleName} has no components") + components.forEach { + when (it.returnType) { + String::class.starProjectedType -> { + val result = it.call(this) as String + assertTrue(result.isNotEmpty(), "${it.name} returned empty string") + if (result.startsWith("https")) + assertTrue(result.startsWith("https://"), "${it.name} has poorly formatted output $result") + } + } + } +} + +fun <T : Comparable<T>> List<T>.assertDescending(tag: String) { + assertEquals(sortedDescending(), this, "$tag not sorted in descending order") +}
\ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/parsers/MessageParserTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/parsers/MessageParserTest.kt index 61c69c40..ecebed04 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/parsers/MessageParserTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/parsers/MessageParserTest.kt @@ -1,6 +1,8 @@ package com.pitchedapps.frost.parsers +import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.facebook.get import org.junit.Test import kotlin.test.assertEquals @@ -15,7 +17,7 @@ class MessageParserTest { @Test fun parseEpoch() { val input = "{\"time\":1507301642,\"short\":true,\"forceseconds\":false}" - assertEquals(1507301642, FrostRegex.epoch.find(input)!!.groupValues[1].toLong()) + assertEquals(1507301642, FB_EPOCH_MATCHER.find(input)[1]!!.toLong()) } @Test diff --git a/app/src/test/kotlin/com/pitchedapps/frost/parsers/ParserTestHelper.kt b/app/src/test/kotlin/com/pitchedapps/frost/parsers/ParserTestHelper.kt index be5ac624..53495ecb 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/parsers/ParserTestHelper.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/parsers/ParserTestHelper.kt @@ -18,5 +18,5 @@ fun <T : Any> T.getResource(path: String): String? { fun <T : Any, P : Any> T.debug(path: String, parser: FrostParser<P>) { val content = getResource("priv/$path.html") ?: return - println(parser.debug(content)) +// println(parser.debug(content)) }
\ No newline at end of file |