1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-10 04:52:38 +01:00

Enhancement/fb requests (#575)

* Update lambdas to references

* Simplify regex and parsers

* Fix some parsing and add more tests

* Improve message parser and tests

* Simplify parser

* Shorten interfaces

* Push rem

* Create notification parser

* Clean up notification service

* Clean up notification service

* Add safe cookie fallback

* Fix cookie reference

* Make parsers only hold cookie string

* Clean up cookie references

* Fix up login and event theme

* Update changelog

Remove workspace backup
This commit is contained in:
Allan Wang 2017-12-26 03:37:32 -05:00
parent 8080d43dbd
commit 1769dbcef9
34 changed files with 663 additions and 336 deletions

2
.gitignore vendored
View File

@ -10,4 +10,4 @@
.externalNativeBuild
*.min.css
.sass-cache/
/projectFilesBackup
/projectFilesBackup

View File

@ -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}"

View File

@ -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;
}

View File

@ -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,

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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; }

View File

@ -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)

View File

@ -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()
}
}

View File

@ -1,5 +1,6 @@
package com.pitchedapps.frost.contracts
import com.pitchedapps.frost.dbflow.CookieModel
import io.reactivex.subjects.PublishSubject
/**

View File

@ -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]*);") }
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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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> {
/**
* 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
*/
fun parse(doc: Document): T?
interface FrostParser<out T : Any> {
/**
* Parse a String input
* Url to request from
*/
fun parse(text: String?): T?
val url: String
/**
* Take in doc and emit debug output
* Call parsing with default implementation using cookie
*/
fun debug(doc: Document): String
fun parse(cookie: String?): ParseResponse<T>?
/**
* Attempts to parse input and emit a debugger
* Call parsing with given document
*/
fun debug(text: String?): String
fun parse(cookie: String?, document: Document): ParseResponse<T>?
/**
* Call parsing with given data
*/
fun parseFromData(cookie: String?, text: String): ParseResponse<T>?
}
internal abstract class FrostParserBase<T> : FrostParser<T> {
data class FrostLink(val text: String, val href: String)
override final fun parse(text: String?): T? {
text ?: return null
data class ParseResponse<out T>(val cookie: String, val data: T) {
override fun toString() = "ParseResponse\ncookie: $cookie\ndata:\n$data"
}
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 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?
// protected abstract fun parse(doc: Document): T?
/**
* 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 ?: ""
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"))
}
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 debugImpl(data: T, result: MutableList<String>)
}
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\\(\"(.*?)\"\\)")
}

View File

@ -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
)
}
}
}
private class MessageParserImpl : FrostParserBase<Triple<List<FrostThread>, FrostLink?, List<FrostLink>>>() {
/**
* [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<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))
}
}

View File

@ -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 &nbsp;
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
)
}
}

View File

@ -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() })
}
}

View File

@ -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

View File

@ -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 &nbsp;
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()

View File

@ -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

View File

@ -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,

View File

@ -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" />

View File

@ -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")
}
}

View File

@ -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")
}
}

View File

@ -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()
}
}

View File

@ -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")
}

View File

@ -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

View File

@ -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))
}

View File

@ -1,13 +1,14 @@
# Changelog
## v1.7.0
## v1.7.1
* Fix launching messages in new overlay
* Fix some errors in launching pages
* Redid base design to prepare for native views
* Automatically bring toolbar up when keyboard is shown
* Rewrite theme components to fully support AMOLED and improve light
* Properly pause webviews when not in use
## v1.6.8
## v1.7.0
* Fully customize your tabs! Check out settings > appearance > main activity tabs
* Optimize scripts
* Add more theme components

View File

@ -18,7 +18,7 @@ TARGET_SDK=27
BUILD_TOOLS=27.0.2
KAU=399a0bf
KOTLIN=1.2.0
KOTLIN=1.2.10
ANDROID_SUPPORT_LIBS=27.0.2
COMMONS_TEXT=1.2