aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle14
-rw-r--r--app/src/main/assets/css/core/main.compact.css2
-rw-r--r--app/src/main/assets/css/core/main.scss2
-rw-r--r--app/src/main/assets/css/themes/custom.compact.css6
-rw-r--r--app/src/main/assets/css/themes/material_amoled.compact.css6
-rw-r--r--app/src/main/assets/css/themes/material_dark.compact.css6
-rw-r--r--app/src/main/assets/css/themes/material_glass.compact.css6
-rw-r--r--app/src/main/assets/css/themes/material_light.compact.css6
-rw-r--r--app/src/main/assets/js/menu.js18
-rw-r--r--app/src/main/assets/js/menu.min.js23
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt34
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt32
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt43
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt12
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt19
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt65
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt23
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/FileChooser.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt16
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt8
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt (renamed from app/src/main/kotlin/com/pitchedapps/frost/facebook/FbTab.kt)4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/UsernameFetcher.kt5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt12
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt50
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt14
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt76
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt105
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt1
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt151
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt14
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Network.kt17
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/JsoupCleaner.kt34
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt31
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt17
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt102
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt12
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt29
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt82
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/HeadlessHtmlExtractor.kt88
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/MessageWebView.kt67
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt33
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/WebStates.kt11
-rw-r--r--app/src/main/res/layout/material_drawer_header.xml119
-rw-r--r--app/src/main/res/values/strings.xml54
-rw-r--r--app/src/main/res/values/strings_about.xml23
-rw-r--r--app/src/main/res/values/strings_download.xml11
-rw-r--r--app/src/main/res/values/strings_play_store.xml25
-rw-r--r--app/src/main/res/values/strings_pref_behaviour.xml4
-rw-r--r--app/src/main/res/values/strings_pref_debug.xml14
-rw-r--r--app/src/main/res/values/strings_pref_experimental.xml3
-rw-r--r--app/src/main/res/values/strings_pref_networks.xml4
-rw-r--r--app/src/main/res/values/strings_pref_notifications.xml1
-rw-r--r--app/src/main/res/values/strings_preferences.xml5
-rw-r--r--app/src/main/res/values/strings_temp.xml6
-rw-r--r--app/src/main/res/values/strings_web_context.xml13
-rw-r--r--app/src/main/res/xml/frost_changelog.xml18
-rw-r--r--app/src/main/res/xml/frost_faq.xml8
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt8
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/utils/JsoupCleanerTest.kt56
-rw-r--r--docs/Changelog.md17
-rw-r--r--gradle.properties14
71 files changed, 1241 insertions, 511 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 624c04ac..920b9fd1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,13 +21,13 @@ android {
buildToolsVersion project.BUILD_TOOLS
androidGitVersion {
- codeFormat = 'MMNNPPBB'
+ codeFormat = 'MMNNPPXX'
format = '%tag%%.count%%-commit%'
prefix 'v'
}
defaultConfig {
- applicationId "${project.APP_GROUP}." + project.APP_ID.toLowerCase()
+ applicationId "${project.APP_GROUP}." + project.APP_ID.toLowerCase(Locale.CANADA)
minSdkVersion Integer.parseInt(project.MIN_SDK)
targetSdkVersion Integer.parseInt(project.TARGET_SDK)
versionCode androidGitVersion.code()
@@ -45,8 +45,7 @@ android {
lintOptions {
warningsAsErrors true
- disable 'LogNotTimber',
- 'TrustAllX509TrustManager',
+ disable 'TrustAllX509TrustManager',
'UnusedResources',
'ContentDescription',
'RtlSymmetry'
@@ -139,6 +138,8 @@ dependencies {
//noinspection GradleDependency
implementation "ca.allanwang.kau:searchview:$KAU"
//noinspection GradleDependency
+ implementation "ca.allanwang.kau:core:$KAU"
+ //noinspection GradleDependency
implementation "ca.allanwang.kau:core-ui:$KAU"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${KOTLIN}"
@@ -177,4 +178,9 @@ dependencies {
implementation "com.sothree.slidinguppanel:library:${SLIDING_PANEL}"
+ //Reactive Libs
+ implementation "io.reactivex.rxjava2:rxkotlin:${RX_KOTLIN}"
+ implementation "io.reactivex.rxjava2:rxandroid:${RX_ANDROID}"
+ implementation "com.github.pwittchen:reactivenetwork-rx2:${RX_NETWORK}"
+
} \ No newline at end of file
diff --git a/app/src/main/assets/css/core/main.compact.css b/app/src/main/assets/css/core/main.compact.css
index c91e892e..de4d4ec5 100644
--- a/app/src/main/assets/css/core/main.compact.css
+++ b/app/src/main/assets/css/core/main.compact.css
@@ -1,6 +1,6 @@
#viewport { background: #451515 !important; }
-body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, .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, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: rgba(255, 0, 255, 0.02) !important; }
+body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, .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, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: rgba(255, 0, 255, 0.02) !important; }
._cv_, ._2sq8 { background-color: rgba(255, 0, 255, 0.02) !important; }
diff --git a/app/src/main/assets/css/core/main.scss b/app/src/main/assets/css/core/main.scss
index 85ce793f..272df88f 100644
--- a/app/src/main/assets/css/core/main.scss
+++ b/app/src/main/assets/css/core/main.scss
@@ -7,7 +7,7 @@
body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8,
.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, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2,
+._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5,
._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel,
diff --git a/app/src/main/assets/css/themes/custom.compact.css b/app/src/main/assets/css/themes/custom.compact.css
index e8add9c8..00663706 100644
--- a/app/src/main/assets/css/themes/custom.compact.css
+++ b/app/src/main/assets/css/themes/custom.compact.css
@@ -1,6 +1,6 @@
#viewport { background: $B$ !important; }
-body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: $BT$ !important; }
+body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, .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, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: $BT$ !important; }
._cv_, ._2sq8 { background-color: $BT$ !important; }
@@ -12,7 +12,7 @@ body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r
button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before, ._590n, ._4g8h, ._2cpp, ._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj, .excessItem, ._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51, ._4qax, .aclb, ._4756, ._w34, ._56bv::before, ._5769, ._34iv, ._z-w, .acbk { background: $BBT$ !important; }
-[style*="color"], body, input, ._42rv, ._4qau, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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; }
._15kl::before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid $D$ !important; }
@@ -20,7 +20,7 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9, ._2u4w, ._3u9t, ._55fj, ._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._3on6, ._2om3, ._2ol-, ._56d8, .al, ._1gkq, ._5fjv, ._5fjw, ._4z83 { border-top: 1px solid $D$ !important; }
-._15ny::after, ._z-w, ._8i2, ._2nk0, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid $D$ !important; }
+._15ny::after, ._z-w, ._8i2, ._2nk0, ._22_8, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid $D$ !important; }
._d4i, ._f6s, .mentions-suggest-item, .mentions-suggest, ._1_y5, ._lr0, ._5hgt, ._2cpp, ._4e8n, ._uww, .mentions-placeholder, .mentions-shadow, .mentions-measurer, ._5whq, ._59tt, ._41ft::after, .jx-tokenizer, ._3uqf, ._4756, ._1rrd, ._5n_f { border: 1px solid $D$ !important; }
diff --git a/app/src/main/assets/css/themes/material_amoled.compact.css b/app/src/main/assets/css/themes/material_amoled.compact.css
index 576e755a..8e57d751 100644
--- a/app/src/main/assets/css/themes/material_amoled.compact.css
+++ b/app/src/main/assets/css/themes/material_amoled.compact.css
@@ -1,6 +1,6 @@
#viewport { background: #000 !important; }
-body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: #000 !important; }
+body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, .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, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: #000 !important; }
._cv_, ._2sq8 { background-color: #000 !important; }
@@ -12,7 +12,7 @@ body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r
button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before, ._590n, ._4g8h, ._2cpp, ._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj, .excessItem, ._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51, ._4qax, .aclb, ._4756, ._w34, ._56bv::before, ._5769, ._34iv, ._z-w, .acbk { background: rgba(89, 89, 89, 0.2) !important; }
-[style*="color"], body, input, ._42rv, ._4qau, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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; }
._15kl::before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(255, 255, 255, 0.3) !important; }
@@ -20,7 +20,7 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9, ._2u4w, ._3u9t, ._55fj, ._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._3on6, ._2om3, ._2ol-, ._56d8, .al, ._1gkq, ._5fjv, ._5fjw, ._4z83 { border-top: 1px solid rgba(255, 255, 255, 0.3) !important; }
-._15ny::after, ._z-w, ._8i2, ._2nk0, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(255, 255, 255, 0.3) !important; }
+._15ny::after, ._z-w, ._8i2, ._2nk0, ._22_8, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(255, 255, 255, 0.3) !important; }
._d4i, ._f6s, .mentions-suggest-item, .mentions-suggest, ._1_y5, ._lr0, ._5hgt, ._2cpp, ._4e8n, ._uww, .mentions-placeholder, .mentions-shadow, .mentions-measurer, ._5whq, ._59tt, ._41ft::after, .jx-tokenizer, ._3uqf, ._4756, ._1rrd, ._5n_f { border: 1px solid rgba(255, 255, 255, 0.3) !important; }
diff --git a/app/src/main/assets/css/themes/material_dark.compact.css b/app/src/main/assets/css/themes/material_dark.compact.css
index 860d01e5..b96e1572 100644
--- a/app/src/main/assets/css/themes/material_dark.compact.css
+++ b/app/src/main/assets/css/themes/material_dark.compact.css
@@ -1,6 +1,6 @@
#viewport { background: #303030 !important; }
-body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: #303030 !important; }
+body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, .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, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: #303030 !important; }
._cv_, ._2sq8 { background-color: #303030 !important; }
@@ -12,7 +12,7 @@ body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r
button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before, ._590n, ._4g8h, ._2cpp, ._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj, .excessItem, ._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51, ._4qax, .aclb, ._4756, ._w34, ._56bv::before, ._5769, ._34iv, ._z-w, .acbk { background: rgba(137, 137, 137, 0.2) !important; }
-[style*="color"], body, input, ._42rv, ._4qau, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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; }
._15kl::before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(255, 255, 255, 0.3) !important; }
@@ -20,7 +20,7 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9, ._2u4w, ._3u9t, ._55fj, ._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._3on6, ._2om3, ._2ol-, ._56d8, .al, ._1gkq, ._5fjv, ._5fjw, ._4z83 { border-top: 1px solid rgba(255, 255, 255, 0.3) !important; }
-._15ny::after, ._z-w, ._8i2, ._2nk0, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(255, 255, 255, 0.3) !important; }
+._15ny::after, ._z-w, ._8i2, ._2nk0, ._22_8, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(255, 255, 255, 0.3) !important; }
._d4i, ._f6s, .mentions-suggest-item, .mentions-suggest, ._1_y5, ._lr0, ._5hgt, ._2cpp, ._4e8n, ._uww, .mentions-placeholder, .mentions-shadow, .mentions-measurer, ._5whq, ._59tt, ._41ft::after, .jx-tokenizer, ._3uqf, ._4756, ._1rrd, ._5n_f { border: 1px solid rgba(255, 255, 255, 0.3) !important; }
diff --git a/app/src/main/assets/css/themes/material_glass.compact.css b/app/src/main/assets/css/themes/material_glass.compact.css
index bd0f5671..f36c2994 100644
--- a/app/src/main/assets/css/themes/material_glass.compact.css
+++ b/app/src/main/assets/css/themes/material_glass.compact.css
@@ -1,6 +1,6 @@
#viewport { background: rgba(0, 0, 0, 0.3) !important; }
-body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: transparent !important; }
+body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, .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, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: transparent !important; }
._cv_, ._2sq8 { background-color: transparent !important; }
@@ -12,7 +12,7 @@ body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r
button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before, ._590n, ._4g8h, ._2cpp, ._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj, .excessItem, ._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51, ._4qax, .aclb, ._4756, ._w34, ._56bv::before, ._5769, ._34iv, ._z-w, .acbk { background: rgba(128, 128, 128, 0.05) !important; }
-[style*="color"], body, input, ._42rv, ._4qau, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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; }
._15kl::before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(255, 255, 255, 0.3) !important; }
@@ -20,7 +20,7 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9, ._2u4w, ._3u9t, ._55fj, ._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._3on6, ._2om3, ._2ol-, ._56d8, .al, ._1gkq, ._5fjv, ._5fjw, ._4z83 { border-top: 1px solid rgba(255, 255, 255, 0.3) !important; }
-._15ny::after, ._z-w, ._8i2, ._2nk0, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(255, 255, 255, 0.3) !important; }
+._15ny::after, ._z-w, ._8i2, ._2nk0, ._22_8, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(255, 255, 255, 0.3) !important; }
._d4i, ._f6s, .mentions-suggest-item, .mentions-suggest, ._1_y5, ._lr0, ._5hgt, ._2cpp, ._4e8n, ._uww, .mentions-placeholder, .mentions-shadow, .mentions-measurer, ._5whq, ._59tt, ._41ft::after, .jx-tokenizer, ._3uqf, ._4756, ._1rrd, ._5n_f { border: 1px solid rgba(255, 255, 255, 0.3) !important; }
diff --git a/app/src/main/assets/css/themes/material_light.compact.css b/app/src/main/assets/css/themes/material_light.compact.css
index f2b086a3..2714fdb7 100644
--- a/app/src/main/assets/css/themes/material_light.compact.css
+++ b/app/src/main/assets/css/themes/material_light.compact.css
@@ -1,6 +1,6 @@
#viewport { background: #fafafa !important; }
-body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, .tlBody, #timelineBody, .timelineX, .timeline, .feed, .tlPrelude, .tlFeedPlaceholder, ._4_d0, .al, ._1gkq, ._5c5b, ._1qxg, ._5luf, ._2new, ._cld, ._3zvb, ._2nk0, ._11ub, ._5p7j, ._55wm, ._5rgs, ._5xuj, ._1sv1, ._45fu, ._18qg, ._1_ac, ._5w3g, ._3e18, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: #fafafa !important; }
+body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4, ._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, .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, ._10c_, ._2jl2, ._5q_r, ._5yt8, ._idb, ._2ip_, ._f6s, ._2l5v, ._8i2, ._kr5, ._d4i, ._577z, ._2u4w, ._3u9p, ._3u9t, ._2v9s, ._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, .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, ._uww, textarea, ._15n_, ._skt, ._5f28, ._14_j, ._3bg5, ._53_-, ._52x1 { background: #fafafa !important; }
._cv_, ._2sq8 { background-color: #fafafa !important; }
@@ -12,7 +12,7 @@ body, #root, #header, [style*="background-color"], ._55wo, ._1upc, input, ._2f9r
button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before, ._590n, ._4g8h, ._2cpp, ._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj, .excessItem, ._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51, ._4qax, .aclb, ._4756, ._w34, ._56bv::before, ._5769, ._34iv, ._z-w, .acbk { background: rgba(255, 255, 255, 0.2) !important; }
-[style*="color"], body, input, ._42rv, ._4qau, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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, ._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, ._5xu2, ._3ml8, ._3mla, ._43mh, .touch .btn, p, span, .fcg, button, ._52j9, ._52jb, ._52ja, ._5j35, ._rnk, ._24u0, ._1g06, ._14ye, .fcb, ._56cz._56c_, ._1gk_, ._55fj, ._45fu, ._18qg, ._1_ac, 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, .mentions-input, .mentions-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; }
._15kl::before, ._5j35::after, ._2k4b, ._3to7, ._4nw8 { border-left: 1px solid rgba(0, 0, 0, 0.3) !important; }
@@ -20,7 +20,7 @@ button:not([style*=image]), button::before, .touch ._56bt, ._56be::before, .btnS
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9, ._2u4w, ._3u9t, ._55fj, ._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._3on6, ._2om3, ._2ol-, ._56d8, .al, ._1gkq, ._5fjv, ._5fjw, ._4z83 { border-top: 1px solid rgba(0, 0, 0, 0.3) !important; }
-._15ny::after, ._z-w, ._8i2, ._2nk0, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(0, 0, 0, 0.3) !important; }
+._15ny::after, ._z-w, ._8i2, ._2nk0, ._22_8, ._2u4w, ._577z:not(:last-child) ._ygd, ._3u9u, ._3mgz, ._52x6, ._2066, ._5luf, .mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .jx-result, ._2om3, ._2ol-, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._1gkq, ._5rgs, ._5xuj, ._1sv1, ._idb, ._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc, ._5pz4, ._5lp4, ._5lp5, ._3on6, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child, ._5fjw > :first-child, ._5fjv, ._5fjw, ._4z83 { border-bottom: 1px solid rgba(0, 0, 0, 0.3) !important; }
._d4i, ._f6s, .mentions-suggest-item, .mentions-suggest, ._1_y5, ._lr0, ._5hgt, ._2cpp, ._4e8n, ._uww, .mentions-placeholder, .mentions-shadow, .mentions-measurer, ._5whq, ._59tt, ._41ft::after, .jx-tokenizer, ._3uqf, ._4756, ._1rrd, ._5n_f { border: 1px solid rgba(0, 0, 0, 0.3) !important; }
diff --git a/app/src/main/assets/js/menu.js b/app/src/main/assets/js/menu.js
index 7394c824..a43b1820 100644
--- a/app/src/main/assets/js/menu.js
+++ b/app/src/main/assets/js/menu.js
@@ -4,6 +4,8 @@ if (!window.hasOwnProperty('frost_menu')) {
window.frost_menu = true;
var viewport = document.querySelector('#viewport');
var root = document.querySelector('#root');
+ if (!viewport) console.log('Menu.js: viewport is null');
+ if (!root) console.log('Menu.js: root is null');
var y = new MutationObserver(function(mutations) {
viewport.removeAttribute('style');
root.removeAttribute('style');
@@ -21,18 +23,24 @@ if (!window.hasOwnProperty('frost_menu')) {
console.log('Found side menu');
while (root.firstChild)
root.removeChild(root.firstChild);
- while (menu.childNodes.length)
- root.appendChild(menu.childNodes[0]);
- Frost.emit(0);
+ while (menu.childNodes.length) {
+ console.log('append');
+ viewport.appendChild(menu.childNodes[0]);
+ }
+ if (typeof Frost !== 'undefined') Frost.emit(0);
setTimeout(function() {
y.disconnect();
console.log('Unhook styler');
}, 500);
}
});
- x.observe(document.querySelector('#mJewelNav'), {
+ var jewel = document.querySelector('#mJewelNav');
+ if (!jewel) console.log('Menu.js: jewel is null');
+ x.observe(jewel, {
childList: true,
subtree: true
});
- document.querySelector('#bookmarks_jewel').querySelector('a').click();
+ var menuA = document.querySelector('#bookmarks_jewel').querySelector('a');
+ if (!menuA) console.log('Menu.js: jewel is null')
+ menuA.click();
}
diff --git a/app/src/main/assets/js/menu.min.js b/app/src/main/assets/js/menu.min.js
index 5403e03a..5b65ac9b 100644
--- a/app/src/main/assets/js/menu.min.js
+++ b/app/src/main/assets/js/menu.min.js
@@ -1,8 +1,10 @@
if(!window.hasOwnProperty("frost_menu")){
console.log("Registering frost_menu"),window.frost_menu=!0
-;var viewport=document.querySelector("#viewport"),root=document.querySelector("#root"),y=new MutationObserver(function(e){
-viewport.removeAttribute("style"),
-root.removeAttribute("style")
+;var viewport=document.querySelector("#viewport"),root=document.querySelector("#root")
+;viewport||console.log("Menu.js: viewport is null"),
+root||console.log("Menu.js: root is null")
+;var y=new MutationObserver(function(e){
+viewport.removeAttribute("style"),root.removeAttribute("style")
})
;y.observe(viewport,{
attributes:!0
@@ -13,14 +15,19 @@ attributes:!0
var o=document.querySelector(".mSideMenu")
;if(null!==o){
for(x.disconnect(),console.log("Found side menu");root.firstChild;)root.removeChild(root.firstChild)
-;for(;o.childNodes.length;)root.appendChild(o.childNodes[0])
-;Frost.emit(0),setTimeout(function(){
+;for(;o.childNodes.length;)console.log("append"),
+viewport.appendChild(o.childNodes[0])
+;"undefined"!=typeof Frost&&Frost.emit(0),setTimeout(function(){
y.disconnect(),console.log("Unhook styler")
},500)
}
-})
-;x.observe(document.querySelector("#mJewelNav"),{
+}),jewel=document.querySelector("#mJewelNav")
+;jewel||console.log("Menu.js: jewel is null"),
+x.observe(jewel,{
childList:!0,
subtree:!0
-}),document.querySelector("#bookmarks_jewel").querySelector("a").click()
+})
+;var menuA=document.querySelector("#bookmarks_jewel").querySelector("a")
+;menuA||console.log("Menu.js: jewel is null"),
+menuA.click()
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
index 371f9c33..4fabf8b8 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
@@ -1,9 +1,12 @@
package com.pitchedapps.frost
+import android.app.Activity
import android.app.Application
import android.graphics.drawable.Drawable
import android.net.Uri
+import android.os.Bundle
import android.widget.ImageView
+import ca.allanwang.kau.logging.KL
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ApplicationVersionSignature
@@ -12,13 +15,12 @@ import com.crashlytics.android.answers.Answers
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.pitchedapps.frost.facebook.FbCookie
-import com.pitchedapps.frost.utils.CrashReportingTree
+import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.Showcase
import com.raizlabs.android.dbflow.config.FlowConfig
import com.raizlabs.android.dbflow.config.FlowManager
import io.fabric.sdk.android.Fabric
-import timber.log.Timber
import java.util.*
@@ -39,22 +41,18 @@ class FrostApp : Application() {
Prefs.initialize(this, "${BuildConfig.APPLICATION_ID}.prefs")
// if (LeakCanary.isInAnalyzerProcess(this)) return
// refWatcher = LeakCanary.install(this)
- if (BuildConfig.DEBUG) {
- Timber.plant(Timber.DebugTree())
-// LeakCanary.enableDisplayLeakActivity(this)
- } else {
+ if (!BuildConfig.DEBUG) {
Fabric.with(this, Crashlytics(), Answers())
Crashlytics.setUserIdentifier(Prefs.frostId)
- Timber.plant(CrashReportingTree())
}
+ KL.debug(BuildConfig.DEBUG)
+ L.debug(BuildConfig.DEBUG)
Prefs.verboseLogging = false
FbCookie()
if (Prefs.installDate == -1L) Prefs.installDate = System.currentTimeMillis()
if (Prefs.identifier == -1) Prefs.identifier = Random().nextInt(Int.MAX_VALUE)
Prefs.lastLaunch = System.currentTimeMillis()
-
-
super.onCreate()
/**
@@ -69,6 +67,24 @@ class FrostApp : Application() {
.thumbnail(old).into(imageView)
}
})
+ if (BuildConfig.DEBUG)
+ registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
+ override fun onActivityPaused(activity: Activity) {}
+ override fun onActivityResumed(activity: Activity) {}
+ override fun onActivityStarted(activity: Activity) {}
+
+ override fun onActivityDestroyed(activity: Activity) {
+ L.d("Activity ${activity.localClassName} destroyed")
+ }
+
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {}
+
+ override fun onActivityStopped(activity: Activity) {}
+
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+ L.d("Activity ${activity.localClassName} created")
+ }
+ })
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
index fbcd12cc..670e8669 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
@@ -1,10 +1,8 @@
package com.pitchedapps.frost.activities
-import android.os.Bundle
import android.support.constraint.ConstraintLayout
import android.support.constraint.ConstraintSet
import android.support.v7.widget.RecyclerView
-import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
@@ -13,8 +11,6 @@ import ca.allanwang.kau.about.LibraryIItem
import ca.allanwang.kau.adapters.FastItemThemedAdapter
import ca.allanwang.kau.adapters.ThemableIItem
import ca.allanwang.kau.adapters.ThemableIItemDelegate
-import ca.allanwang.kau.animators.FadeScaleAnimatorAdd
-import ca.allanwang.kau.animators.KauAnimator
import ca.allanwang.kau.utils.*
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
@@ -26,10 +22,8 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
+import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
-import org.jetbrains.anko.doAsync
-import org.jetbrains.anko.uiThread
-import java.security.InvalidParameterException
/**
@@ -41,6 +35,7 @@ class AboutActivity : AboutActivityBase(null, {
backgroundColor = Prefs.bgColor.withMinAlpha(200)
cutoutForeground = if (0xff3b5998.toInt().isColorVisibleOn(Prefs.bgColor)) 0xff3b5998.toInt() else Prefs.accentColor
cutoutDrawableRes = R.drawable.frost_f_256
+ faqPageTitleRes = R.string.faq_title
faqXmlRes = R.xml.frost_faq
faqParseNewLine = false
}) {
@@ -60,6 +55,7 @@ class AboutActivity : AboutActivityBase(null, {
"kotterknife",
"materialdialogs",
"materialdrawer",
+ "rxjava",
"subsamplingscaleimageview"
)
@@ -68,6 +64,9 @@ class AboutActivity : AboutActivityBase(null, {
return l
}
+ var lastClick = -1L
+ var clickCount = 0
+
override fun postInflateMainPage(adapter: FastItemThemedAdapter<IItem<*, *>>) {
/**
* Frost may not be a library but we're conveying the same info
@@ -85,7 +84,22 @@ class AboutActivity : AboutActivityBase(null, {
}
}
adapter.add(LibraryIItem(frost)).add(AboutLinks())
-
+ adapter.withOnClickListener { _, _, item, _ ->
+ if (item is LibraryIItem) {
+ val now = System.currentTimeMillis()
+ if (now - lastClick > 500)
+ clickCount = 0
+ else
+ clickCount++
+ lastClick = now
+ if (clickCount == 7 && !Prefs.debugSettings) {
+ Prefs.debugSettings = true
+ L.d("Debugging section enabled")
+ toast(R.string.debug_toast_enabled)
+ }
+ }
+ false
+ }
}
class AboutLinks : AbstractItem<AboutLinks, AboutLinks.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
@@ -128,7 +142,7 @@ class AboutActivity : AboutActivityBase(null, {
setImageDrawable(icon.toDrawable(context, 32))
scaleType = ImageView.ScaleType.CENTER
background = context.resolveDrawable(android.R.attr.selectableItemBackgroundBorderless)
- setOnClickListener({ onClick() })
+ setOnClickListener { onClick() }
container.addView(this)
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
index 77a20d04..c7ca5ec7 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseActivity.kt
@@ -2,10 +2,16 @@ package com.pitchedapps.frost.activities
import android.os.Bundle
import ca.allanwang.kau.internal.KauBaseActivity
+import com.github.pwittchen.reactivenetwork.library.rx2.Connectivity
+import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.pitchedapps.frost.R
+import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.materialDialogThemed
import com.pitchedapps.frost.utils.setFrostTheme
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
/**
* Created by Allan Wang on 2017-06-12.
@@ -29,4 +35,41 @@ abstract class BaseActivity : KauBaseActivity() {
setFrostTheme()
}
+ private var networkDisposable: Disposable? = null
+ private var networkConsumer: ((Connectivity) -> Unit)? = null
+
+ fun setNetworkObserver(consumer: (connectivity: Connectivity) -> Unit) {
+ this.networkConsumer = consumer
+ }
+
+ fun observeNetworkConnectivity() {
+ val consumer = networkConsumer ?: return
+ networkDisposable = ReactiveNetwork.observeNetworkConnectivity(applicationContext)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
+ connectivity: Connectivity ->
+ connectivity.apply {
+ L.d("Network connectivity changed: isAvailable: $isAvailable isRoaming: $isRoaming")
+ consumer(connectivity)
+ }
+ }
+ }
+
+ fun disposeNetworkConnectivity() {
+ if (!(networkDisposable?.isDisposed ?: true))
+ networkDisposable?.dispose()
+ networkDisposable = null
+ }
+
+ override fun onResume() {
+ super.onResume()
+ disposeNetworkConnectivity()
+ observeNetworkConnectivity()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ disposeNetworkConnectivity()
+ }
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
index 6a39b269..31479d54 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
@@ -7,7 +7,6 @@ import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
-import android.os.Environment
import android.support.design.widget.FloatingActionButton
import android.support.v4.content.FileProvider
import android.view.View
@@ -34,11 +33,8 @@ import com.pitchedapps.frost.utils.*
import com.sothree.slidinguppanel.SlidingUpPanelLayout
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
-import timber.log.Timber
import java.io.File
import java.io.IOException
-import java.text.SimpleDateFormat
-import java.util.*
/**
* Created by Allan Wang on 2017-07-15.
@@ -99,8 +95,8 @@ class ImageActivity : KauBaseActivity() {
})
fab.setOnClickListener { fabAction.onClick(this) }
photo.setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
- override fun onImageLoadError(e: Exception) {
- L.e(e, "Image load error")
+ override fun onImageLoadError(e: Exception?) {
+ e.logFrostAnswers("Image load error")
imageCallback(null, false)
}
})
@@ -155,7 +151,7 @@ class ImageActivity : KauBaseActivity() {
callback(null)
} else {
tempFilePath = photoFile.absolutePath
- Timber.d("Temp image path $tempFilePath")
+ L.d("Temp image path $tempFilePath")
// File created; proceed with request
val photoURI = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".provider",
@@ -252,7 +248,7 @@ internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconC
}
activity.startActivity(intent)
} catch (e: Exception) {
- L.e(e, "Image share failed");
+ e.logFrostAnswers("Image share failed")
activity.snackbar(R.string.image_share_failed)
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
index 47c286fa..67f07635 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
@@ -12,6 +12,7 @@ import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.fadeIn
import ca.allanwang.kau.utils.fadeOut
import com.bumptech.glide.Glide
+import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
@@ -44,6 +45,7 @@ class LoginActivity : BaseActivity() {
val profileObservable = SingleSubject.create<Boolean>()
val usernameObservable = SingleSubject.create<String>()
+ lateinit var profileLoader: RequestManager
// Helper to set and enable swipeRefresh
var refresh: Boolean
@@ -68,6 +70,7 @@ class LoginActivity : BaseActivity() {
loadInfo(cookie)
})
}
+ profileLoader = Glide.with(profile)
}
fun loadInfo(cookie: CookieModel) {
@@ -78,8 +81,8 @@ class LoginActivity : BaseActivity() {
(foundImage, name) ->
refresh = false
if (!foundImage) {
- L.eThrow("Could not get profile photo; Invalid userId?")
- L.i("-\t$cookie")
+ L.e("Could not get profile photo; Invalid userId?")
+ L.i(null, cookie.toString())
}
textview.text = String.format(getString(R.string.welcome), name)
textview.fadeIn()
@@ -102,14 +105,14 @@ class LoginActivity : BaseActivity() {
fun loadProfile(id: Long) {
- Glide.with(profile).load(PROFILE_PICTURE_URL(id)).withRoundIcon().listener(object : RequestListener<Drawable> {
+ 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)
return false
}
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
- if (e != null) L.e(e, "Profile loading exception")
+ e.logFrostAnswers( "Profile loading exception")
profileObservable.onSuccess(false)
return false
}
@@ -119,12 +122,4 @@ class LoginActivity : BaseActivity() {
fun loadUsername(cookie: CookieModel) {
cookie.fetchUsername { usernameObservable.onSuccess(it) }
}
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (requestCode == 999) {
- L.d("Result found for activity with result $resultCode")
- L.d("Intent data ${data?.extras.toString()}")
- } else
- super.onActivityResult(requestCode, resultCode, data)
- }
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
index e8148b55..759be983 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
@@ -50,7 +50,7 @@ import com.pitchedapps.frost.dbflow.loadFbCookie
import com.pitchedapps.frost.dbflow.loadFbTabs
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbCookie.switchUser
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.utils.*
@@ -60,6 +60,7 @@ import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import com.pitchedapps.frost.views.BadgedIcon
import com.pitchedapps.frost.views.FrostViewPager
import com.pitchedapps.frost.web.SearchWebView
+import com.pitchedapps.frost.web.shouldLoadImages
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
@@ -160,14 +161,10 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
// }
setFrostColors(toolbar, themeWindow = false, headers = arrayOf(tabs, appBar), backgrounds = arrayOf(viewPager))
onCreateBilling()
- if (Prefs.installDate < 1501454310304 && Showcase.intro)
- materialDialogThemed {
- title(R.string.intro_title)
- content(R.string.intro_desc)
- positiveText(R.string.kau_yes)
- negativeText(R.string.kau_no)
- onPositive { _, _ -> launchIntroActivity(cookies()) }
- }
+ setNetworkObserver {
+ connectivity ->
+ shouldLoadImages = !connectivity.isRoaming
+ }
}
fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) {
@@ -206,10 +203,10 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
tabsForEachView {
_, view ->
when (view.iicon) {
- FbTab.FEED.icon -> view.badgeText = feed
- FbTab.FRIENDS.icon -> view.badgeText = requests
- FbTab.MESSAGES.icon -> view.badgeText = messages
- FbTab.NOTIFICATIONS.icon -> view.badgeText = notifications
+ FbItem.FEED.icon -> view.badgeText = feed
+ FbItem.FRIENDS.icon -> view.badgeText = requests
+ FbItem.MESSAGES.icon -> view.badgeText = messages
+ FbItem.NOTIFICATIONS.icon -> view.badgeText = notifications
}
}
}
@@ -230,10 +227,10 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
translucentStatusBar = false
sliderBackgroundColor = navBg
drawerHeader = accountHeader {
+ customViewRes = R.layout.material_drawer_header
textColor = Prefs.iconColor.toLong()
backgroundDrawable = ColorDrawable(navHeader)
selectionSecondLineShown = false
- paddingBelow = false
cookies().forEach { (id, name) ->
profile(name = name ?: "") {
iconUrl = PROFILE_PICTURE_URL(id)
@@ -261,7 +258,7 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
identifier = -4L
}
onProfileChanged { _, profile, current ->
- if (current) launchWebOverlay(FbTab.PROFILE.url)
+ if (current) launchWebOverlay(FbItem.PROFILE.url)
else when (profile.identifier) {
-2L -> {
val currentCookie = loadFbCookie(Prefs.userId)
@@ -295,25 +292,25 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
}
}
drawerHeader.setActiveProfile(Prefs.userId)
- primaryFrostItem(FbTab.FEED_MOST_RECENT)
- primaryFrostItem(FbTab.FEED_TOP_STORIES)
- primaryFrostItem(FbTab.ACTIVITY_LOG)
+ primaryFrostItem(FbItem.FEED_MOST_RECENT)
+ primaryFrostItem(FbItem.FEED_TOP_STORIES)
+ primaryFrostItem(FbItem.ACTIVITY_LOG)
divider()
- primaryFrostItem(FbTab.PHOTOS)
- primaryFrostItem(FbTab.GROUPS)
- primaryFrostItem(FbTab.FRIENDS)
- primaryFrostItem(FbTab.PAGES)
+ primaryFrostItem(FbItem.PHOTOS)
+ primaryFrostItem(FbItem.GROUPS)
+ primaryFrostItem(FbItem.FRIENDS)
+ primaryFrostItem(FbItem.PAGES)
divider()
- primaryFrostItem(FbTab.EVENTS)
- primaryFrostItem(FbTab.BIRTHDAYS)
- primaryFrostItem(FbTab.ON_THIS_DAY)
+ primaryFrostItem(FbItem.EVENTS)
+ primaryFrostItem(FbItem.BIRTHDAYS)
+ primaryFrostItem(FbItem.ON_THIS_DAY)
divider()
- primaryFrostItem(FbTab.NOTES)
- primaryFrostItem(FbTab.SAVED)
+ primaryFrostItem(FbItem.NOTES)
+ primaryFrostItem(FbItem.SAVED)
}
}
- fun Builder.primaryFrostItem(item: FbTab) = this.primaryItem(item.titleId) {
+ fun Builder.primaryFrostItem(item: FbItem) = this.primaryItem(item.titleId) {
iicon = item.icon
iconColor = Prefs.textColor.toLong()
textColor = Prefs.textColor.toLong()
@@ -349,6 +346,7 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
override fun searchOverlayDispose() {
hiddenSearchView?.dispose()
hiddenSearchView = null
+ searchView?.unBind { launchWebOverlay(FbItem.SEARCH.url); true }
searchView = null
}
@@ -369,10 +367,7 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
if (Prefs.searchBar) {
if (firstLoadFinished && hiddenSearchView == null) hiddenSearchView = SearchWebView(this, this)
if (searchView == null) searchView = bindSearchView(menu, R.id.action_search, Prefs.iconColor) {
- textObserver = {
- observable, _ ->
- observable.observeOn(AndroidSchedulers.mainThread()).subscribe { hiddenSearchView?.query(it) }
- }
+ textCallback = { query, _ -> runOnUiThread { hiddenSearchView?.query(query) } }
foregroundColor = Prefs.textColor
backgroundColor = Prefs.bgColor.withMinAlpha(200)
openListener = { hiddenSearchView?.pauseLoad = false }
@@ -380,8 +375,8 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
onItemClick = { _, key, _, _ -> launchWebOverlay(key) }
}
} else {
- searchOverlayDispose()
- menu.findItem(R.id.action_search).setOnMenuItemClickListener { _ -> launchWebOverlay(FbTab.SEARCH.url); true }
+ if (searchView != null) searchOverlayDispose()
+ else menu.findItem(R.id.action_search).setOnMenuItemClickListener { _ -> launchWebOverlay(FbItem.SEARCH.url); true }
}
return true
}
@@ -461,7 +456,7 @@ class MainActivity : BaseActivity(), SearchWebView.SearchContract,
val currentFragment
get() = supportFragmentManager.findFragmentByTag("android:switcher:${R.id.container}:${viewPager.currentItem}") as WebFragment
- inner class SectionsPagerAdapter(fm: FragmentManager, val pages: List<FbTab>) : FragmentPagerAdapter(fm) {
+ inner class SectionsPagerAdapter(fm: FragmentManager, val pages: List<FbItem>) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
val fragment = WebFragment(pages[position], position)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
index 7cbbe4df..196aa461 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
@@ -6,16 +6,12 @@ import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
-import ca.allanwang.kau.about.kauLaunchAbout
import ca.allanwang.kau.kpref.activity.CoreAttributeContract
import ca.allanwang.kau.kpref.activity.KPrefActivity
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import ca.allanwang.kau.kpref.activity.items.KPrefItemBase
import ca.allanwang.kau.ui.views.RippleCanvas
-import ca.allanwang.kau.utils.finishSlideOut
-import ca.allanwang.kau.utils.setMenuIcons
-import ca.allanwang.kau.utils.string
-import ca.allanwang.kau.utils.tint
+import ca.allanwang.kau.utils.*
import ca.allanwang.kau.xml.showChangelog
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
@@ -38,7 +34,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (!onActivityResultBilling(requestCode, resultCode, data))
super.onActivityResult(requestCode, resultCode, data)
- adapter.notifyDataSetChanged()
+ reloadList()
}
override fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit = {
@@ -67,6 +63,11 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
iicon = GoogleMaterial.Icon.gmd_notifications
}
+ subItems(R.string.network, getNetworkPrefs()) {
+ descRes = R.string.network_desc
+ iicon = GoogleMaterial.Icon.gmd_network_cell
+ }
+
subItems(R.string.experimental, getExperimentalPrefs()) {
descRes = R.string.experimental_desc
iicon = CommunityMaterial.Icon.cmd_flask_outline
@@ -81,7 +82,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
plainText(R.string.about_frost) {
descRes = R.string.about_frost_desc
iicon = GoogleMaterial.Icon.gmd_info
- onClick = { _, _, _ -> kauLaunchAbout(AboutActivity::class.java); true }
+ onClick = { _, _, _ -> startActivityForResult(AboutActivity::class.java, 9, true); true }
}
plainText(R.string.replay_intro) {
@@ -89,6 +90,12 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
onClick = { _, _, _ -> launchIntroActivity(cookies()); true }
}
+ subItems(R.string.debug_frost, getDebugPrefs()) {
+ descRes = R.string.debug_frost_desc
+ iicon = CommunityMaterial.Icon.cmd_android_debug_bridge
+ visible = { Prefs.debugSettings }
+ }
+
if (BuildConfig.DEBUG) {
checkbox(R.string.custom_pro, { Prefs.debugPro }, { Prefs.debugPro = it })
}
@@ -130,8 +137,6 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_settings, menu)
toolbar.tint(Prefs.iconColor)
- toolbarTitle.textColor = Prefs.iconColor
- toolbarTitle.invalidate()
setMenuIcons(menu, Prefs.iconColor,
R.id.action_email to GoogleMaterial.Icon.gmd_email,
R.id.action_changelog to GoogleMaterial.Icon.gmd_info)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FileChooser.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FileChooser.kt
index fd8a3677..f3d90bcc 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/FileChooser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/FileChooser.kt
@@ -5,8 +5,6 @@ import android.content.Intent
import android.net.Uri
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
-import ca.allanwang.kau.mediapicker.MediaPickerActivityOverlayBase
-import ca.allanwang.kau.mediapicker.MediaType
import ca.allanwang.kau.mediapicker.kauLaunchMediaPicker
import ca.allanwang.kau.mediapicker.kauOnMediaPickerResult
import com.pitchedapps.frost.activities.ImagePickerActivity
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
index 901ba02d..92cdf503 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
@@ -2,16 +2,18 @@ package com.pitchedapps.frost.dbflow
import android.os.Parcel
import android.os.Parcelable
+import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.pitchedapps.frost.facebook.FACEBOOK_COM
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.logFrostAnswers
import com.raizlabs.android.dbflow.annotation.ConflictAction
import com.raizlabs.android.dbflow.annotation.Database
import com.raizlabs.android.dbflow.annotation.PrimaryKey
import com.raizlabs.android.dbflow.annotation.Table
import com.raizlabs.android.dbflow.kotlinextensions.*
import com.raizlabs.android.dbflow.structure.BaseModel
-import org.jetbrains.anko.doAsync
+import io.reactivex.schedulers.Schedulers
import org.jsoup.Jsoup
import paperparcel.PaperParcel
import java.net.UnknownHostException
@@ -64,20 +66,22 @@ fun removeCookie(id: Long) {
}
fun CookieModel.fetchUsername(callback: (String) -> Unit) {
- doAsync {
+ ReactiveNetwork.checkInternetConnectivity().subscribeOn(Schedulers.io()).subscribe {
+ yes, _ ->
+ if (!yes) return@subscribe callback("")
var result = ""
try {
- result = Jsoup.connect(FbTab.PROFILE.url)
+ result = Jsoup.connect(FbItem.PROFILE.url)
.cookie(FACEBOOK_COM, cookie)
.get().title()
L.d("Fetch username found", result)
} catch (e: Exception) {
if (e !is UnknownHostException)
- L.e(e, "Fetch username failed")
+ e.logFrostAnswers("Fetch username failed")
} finally {
if (result.isBlank() && (name?.isNotBlank() ?: false)) {
callback(name!!)
- return@doAsync
+ return@subscribe
}
if (name != result) {
name = result
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt
index 69c7f3d5..4b2d3403 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt
@@ -1,7 +1,7 @@
package com.pitchedapps.frost.dbflow
import android.content.Context
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.defaultTabs
import com.pitchedapps.frost.utils.L
import com.raizlabs.android.dbflow.annotation.Database
@@ -22,15 +22,15 @@ object FbTabsDb {
}
@Table(database = FbTabsDb::class, allFields = true)
-data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbTab = FbTab.FEED) : BaseModel()
+data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel()
-fun loadFbTabs(): List<FbTab> {
+fun loadFbTabs(): List<FbItem> {
val tabs: List<FbTabModel>? = SQLite.select().from(FbTabModel::class).orderBy(FbTabModel_Table.position, true).queryList()
if (tabs?.isNotEmpty() ?: false) return tabs!!.map { it.tab }
L.d("No tabs; loading default")
return defaultTabs()
}
-fun List<FbTab>.saveAsync(c: Context) {
+fun List<FbItem>.saveAsync(c: Context) {
mapIndexed { index, fbTab -> FbTabModel(index, fbTab) }.replace(c, FbTabsDb.NAME)
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbTab.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
index d1f0b046..a4736091 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbTab.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbItem.kt
@@ -10,7 +10,7 @@ import com.pitchedapps.frost.web.FrostWebViewClient
import com.pitchedapps.frost.web.FrostWebViewClientMenu
import com.pitchedapps.frost.web.FrostWebViewCore
-enum class FbTab(@StringRes val titleId: Int, val icon: IIcon, relativeUrl: String, val webClient: ((webCore: FrostWebViewCore) -> FrostWebViewClient)? = null) {
+enum class FbItem(@StringRes val titleId: Int, val icon: IIcon, relativeUrl: String, val webClient: ((webCore: FrostWebViewCore) -> FrostWebViewClient)? = null) {
ACTIVITY_LOG(R.string.activity_log, GoogleMaterial.Icon.gmd_list, "me/allactivity"),
BIRTHDAYS(R.string.birthdays, GoogleMaterial.Icon.gmd_cake, "events/birthdays"),
CHAT(R.string.chat, GoogleMaterial.Icon.gmd_chat, "buddylist"),
@@ -36,4 +36,4 @@ enum class FbTab(@StringRes val titleId: Int, val icon: IIcon, relativeUrl: Stri
val url = "$FB_URL_BASE$relativeUrl"
}
-fun defaultTabs(): List<FbTab> = listOf(FbTab.FEED, FbTab.MESSAGES, FbTab.NOTIFICATIONS, FbTab.MENU)
+fun defaultTabs(): List<FbItem> = listOf(FbItem.FEED, FbItem.MESSAGES, FbItem.NOTIFICATIONS, FbItem.MENU)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt
index 7cd93d14..69b2ba41 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt
@@ -33,7 +33,7 @@ class FbUrlFormatter(url: String) {
if (cleanedUrl.startsWith("#!/")) cleanedUrl = cleanedUrl.substring(2)
if (cleanedUrl.startsWith("/")) cleanedUrl = FB_URL_BASE + cleanedUrl.substring(1)
cleanedUrl = cleanedUrl.replaceFirst(".facebook.com//", ".facebook.com/") //sometimes we are given a bad url
- L.v("Formatted url from $url to $cleanedUrl")
+ L.v(null, "Formatted url from $url to $cleanedUrl")
cleaned = cleanedUrl
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/UsernameFetcher.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/UsernameFetcher.kt
index dfdfa027..f2bcc328 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/UsernameFetcher.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/UsernameFetcher.kt
@@ -3,6 +3,7 @@ package com.pitchedapps.frost.facebook
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.dbflow.saveFbCookie
import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.logFrostAnswers
import io.reactivex.subjects.SingleSubject
import org.jsoup.Jsoup
import kotlin.concurrent.thread
@@ -16,12 +17,12 @@ object UsernameFetcher {
thread {
var name = ""
try {
- name = Jsoup.connect(FbTab.PROFILE.url)
+ name = Jsoup.connect(FbItem.PROFILE.url)
.cookie(FACEBOOK_COM, data.cookie)
.get().title()
L.d("User name found", name)
} catch (e: Exception) {
- L.e(e, "User name fetching failed")
+ e.logFrostAnswers("User name fetching failed")
} finally {
data.name = name
saveFbCookie(data)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt
index 239f5842..920052f9 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/WebFragment.kt
@@ -8,7 +8,7 @@ import android.view.View
import android.view.ViewGroup
import ca.allanwang.kau.utils.withArguments
import com.pitchedapps.frost.activities.MainActivity
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.FeedSort
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.web.FrostWebView
@@ -30,15 +30,15 @@ class WebFragment : Fragment() {
const val REQUEST_TEXT_ZOOM = 17
const val REQUEST_REFRESH = 99
- operator fun invoke(data: FbTab, position: Int) = WebFragment().withArguments(
+ operator fun invoke(data: FbItem, position: Int) = WebFragment().withArguments(
ARG_URL to data.url,
ARG_POSITION to position,
ARG_URL_ENUM to when (data) {
//If is feed, check if sorting method is specified
- FbTab.FEED -> when (FeedSort(Prefs.feedSort)) {
+ FbItem.FEED -> when (FeedSort(Prefs.feedSort)) {
FeedSort.DEFAULT -> data
- FeedSort.MOST_RECENT -> FbTab.FEED_MOST_RECENT
- FeedSort.TOP -> FbTab.FEED_TOP_STORIES
+ FeedSort.MOST_RECENT -> FbItem.FEED_MOST_RECENT
+ FeedSort.TOP -> FbItem.FEED_TOP_STORIES
}
else -> data
})
@@ -47,7 +47,7 @@ class WebFragment : Fragment() {
// val refresh: SwipeRefreshLayout by lazy { frostWebView.refresh }
val web: FrostWebViewCore by lazy { frostWebView.web }
val url: String by lazy { arguments.getString(ARG_URL) }
- val urlEnum: FbTab by lazy { arguments.getSerializable(ARG_URL_ENUM) as FbTab }
+ val urlEnum: FbItem by lazy { arguments.getSerializable(ARG_URL_ENUM) as FbItem }
val position: Int by lazy { arguments.getInt(ARG_POSITION) }
lateinit var frostWebView: FrostWebView
private var firstLoad = true
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt
index 0992a9cb..733bc151 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssAssets.kt
@@ -3,7 +3,10 @@ package com.pitchedapps.frost.injectors
import android.graphics.Color
import android.webkit.WebView
import ca.allanwang.kau.utils.*
+import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
+import java.io.FileNotFoundException
+import java.util.*
/**
* Created by Allan Wang on 2017-05-31.
@@ -14,33 +17,38 @@ enum class CssAssets(val folder: String = "themes") : InjectorContract {
MATERIAL_LIGHT, MATERIAL_DARK, MATERIAL_AMOLED, MATERIAL_GLASS, CUSTOM, ROUND_ICONS("components")
;
- var file = "${name.toLowerCase()}.compact.css"
+ var file = "${name.toLowerCase(Locale.CANADA)}.compact.css"
var injector: JsInjector? = null
override fun inject(webView: WebView, callback: ((String) -> Unit)?) {
if (injector == null) {
- var content = webView.context.assets.open("css/$folder/$file").bufferedReader().use { it.readText() }
- if (this == CUSTOM) {
- var bbt = Prefs.bgColor
- val bt: String
- if (Color.alpha(bbt) == 255) {
- bbt = bbt.adjustAlpha(0.2f).colorToForeground(0.35f)
- bt = Prefs.bgColor.toRgbaString()
- } else {
- bbt = bbt.adjustAlpha(0.05f).colorToForeground(0.5f)
- bt = "transparent"
+ try {
+ var content = webView.context.assets.open("css/$folder/$file").bufferedReader().use { it.readText() }
+ if (this == CUSTOM) {
+ var bbt = Prefs.bgColor
+ val bt: String
+ if (Color.alpha(bbt) == 255) {
+ bbt = bbt.adjustAlpha(0.2f).colorToForeground(0.35f)
+ bt = Prefs.bgColor.toRgbaString()
+ } else {
+ bbt = bbt.adjustAlpha(0.05f).colorToForeground(0.5f)
+ bt = "transparent"
+ }
+ content = content
+ .replace("\$T\$", Prefs.textColor.toRgbaString())
+ .replace("\$TT\$", Prefs.textColor.colorToBackground(0.05f).toRgbaString())
+ .replace("\$B\$", Prefs.bgColor.toRgbaString())
+ .replace("\$BT\$", bt)
+ .replace("\$BBT\$", bbt.toRgbaString())
+ .replace("\$O\$", Prefs.bgColor.withAlpha(255).toRgbaString())
+ .replace("\$OO\$", Prefs.bgColor.colorToForeground(0.35f).withAlpha(255).toRgbaString())
+ .replace("\$D\$", Prefs.textColor.adjustAlpha(0.3f).toRgbaString())
}
- content = content
- .replace("\$T\$", Prefs.textColor.toRgbaString())
- .replace("\$TT\$", Prefs.textColor.colorToBackground(0.05f).toRgbaString())
- .replace("\$B\$", Prefs.bgColor.toRgbaString())
- .replace("\$BT\$", bt)
- .replace("\$BBT\$", bbt.toRgbaString())
- .replace("\$O\$", Prefs.bgColor.withAlpha(255).toRgbaString())
- .replace("\$OO\$", Prefs.bgColor.colorToForeground(0.35f).withAlpha(255).toRgbaString())
- .replace("\$D\$", Prefs.textColor.adjustAlpha(0.3f).toRgbaString())
+ injector = JsBuilder().css(content).build()
+ } catch (e: FileNotFoundException) {
+ L.e(e, "CssAssets file not found")
+ injector = JsInjector(JsActions.EMPTY.function)
}
- injector = JsBuilder().css(content).build()
}
injector!!.inject(webView, callback)
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt
index fae1846b..3fa03bcc 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt
@@ -14,7 +14,8 @@ enum class JsActions(body: String) : InjectorContract {
* see [com.pitchedapps.frost.web.FrostJSI.loadLogin]
*/
LOGIN_CHECK("document.getElementById('signup-button')&&Frost.loadLogin();"),
- BASE_HREF("document.write(\"<base href='$FB_URL_BASE'/>\");"),
+ BASE_HREF("""document.write("<base href='$FB_URL_BASE'/>");"""),
+ FETCH_BODY("""setTimeout(function(){var e=document.querySelector("main");e||(e=document.querySelector("body")),Frost.handleHtml(e.outerHTML)},1e2);"""),
/**
* Used as a pseudoinjector for maybe functions
*/
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt
index d2201c52..27b0f92a 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt
@@ -1,6 +1,9 @@
package com.pitchedapps.frost.injectors
import android.webkit.WebView
+import com.pitchedapps.frost.utils.L
+import java.io.FileNotFoundException
+import java.util.*
/**
* Created by Allan Wang on 2017-05-31.
@@ -11,13 +14,18 @@ enum class JsAssets : InjectorContract {
MENU, CLICK_A, CONTEXT_A, HEADER_BADGES, SEARCH, TEXTAREA_LISTENER, NOTIF_MSG
;
- var file = "${name.toLowerCase()}.min.js"
+ var file = "${name.toLowerCase(Locale.CANADA)}.min.js"
var injector: JsInjector? = null
override fun inject(webView: WebView, callback: ((String) -> Unit)?) {
if (injector == null) {
- val content = webView.context.assets.open("js/$file").bufferedReader().use { it.readText() }
- injector = JsBuilder().js(content).build()
+ try {
+ val content = webView.context.assets.open("js/$file").bufferedReader().use { it.readText() }
+ injector = JsBuilder().js(content).build()
+ } catch (e: FileNotFoundException) {
+ L.e(e, "JsAssets file not found")
+ injector = JsInjector(JsActions.EMPTY.function)
+ }
}
injector!!.inject(webView, callback)
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
index 2453d3b0..d3dfe79c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
@@ -21,7 +21,6 @@ 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.fetchUsername
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.utils.ARG_USER_ID
import com.pitchedapps.frost.utils.L
@@ -31,6 +30,12 @@ import org.jetbrains.anko.runOnUiThread
/**
* Created by Allan Wang on 2017-07-08.
+ *
+ * Logic for build notifications, scheduling notifications, and showing notifications
+ */
+
+/**
+ * Wrap the default builder with our icon and accent color
*/
val Context.frostNotification: NotificationCompat.Builder
get() = NotificationCompat.Builder(this, BuildConfig.APPLICATION_ID).apply {
@@ -39,6 +44,9 @@ val Context.frostNotification: NotificationCompat.Builder
color = color(R.color.frost_notification_accent)
}
+/**
+ * Assign global changes to the notification after it is built
+ */
@Suppress("DEPRECATION")
//The update feature is for Android O and seems to still be in beta
fun Notification.frostConfig() = apply {
@@ -54,6 +62,7 @@ val NotificationCompat.Builder.withBigText: NotificationCompat.BigTextStyle
* Created by Allan Wang on 2017-07-08.
*
* Custom target to set the content view and update a given notification
+ * 40dp is the size of the right avatar
*/
class FrostNotificationTarget(val context: Context,
val notifId: Int,
@@ -67,6 +76,9 @@ class FrostNotificationTarget(val context: Context,
}
}
+internal const val FROST_NOTIFICATION_GROUP = "frost"
+internal const val FROST_MESSAGE_NOTIFICATION_GROUP = "frost_im"
+
/**
* Notification data holder
*/
@@ -77,39 +89,35 @@ data class NotificationContent(val data: CookieModel,
val text: String,
val timestamp: Long,
val profileUrl: String) {
- fun createNotification(context: Context, verifiedUser: Boolean = false) {
- //in case we haven't found the name, we will try one more time before passing the notification
- if (!verifiedUser && data.name?.isBlank() ?: true) {
- data.fetchUsername {
- data.name = it
- createNotification(context, true)
- }
- } else {
- val intent = Intent(context, FrostWebActivity::class.java)
- intent.data = Uri.parse(href.formattedFbUrl)
- intent.putExtra(ARG_USER_ID, data.id)
- val group = "frost_${data.id}"
- val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
- val notifBuilder = context.frostNotification
- .setContentTitle(title ?: context.string(R.string.frost_name))
- .setContentText(text)
- .setContentIntent(pendingIntent)
- .setCategory(Notification.CATEGORY_SOCIAL)
- .setSubText(data.name)
- .setGroup(group)
-
- if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000)
- L.v("Notif load $this")
- NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.withBigText.build().frostConfig())
-
- if (profileUrl.isNotBlank()) {
- context.runOnUiThread {
- Glide.with(context)
- .asBitmap()
- .load(profileUrl)
- .withRoundIcon()
- .into(FrostNotificationTarget(context, notifId, group, notifBuilder))
- }
+ fun createNotification(context: Context) = createNotification(context, FROST_NOTIFICATION_GROUP)
+
+ fun createMessageNotification(context: Context) = createNotification(context, FROST_MESSAGE_NOTIFICATION_GROUP)
+
+ private fun createNotification(context: Context, groupPrefix: String) {
+ val intent = Intent(context, FrostWebActivity::class.java)
+ intent.data = Uri.parse(href.formattedFbUrl)
+ intent.putExtra(ARG_USER_ID, data.id)
+ val group = "${groupPrefix}_${data.id}"
+ val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
+ val notifBuilder = context.frostNotification
+ .setContentTitle(title ?: context.string(R.string.frost_name))
+ .setContentText(text)
+ .setContentIntent(pendingIntent)
+ .setCategory(Notification.CATEGORY_SOCIAL)
+ .setSubText(data.name)
+ .setGroup(group)
+
+ if (timestamp != -1L) notifBuilder.setWhen(timestamp * 1000)
+ L.v("Notif load", this.toString())
+ NotificationManagerCompat.from(context).notify(group, notifId, notifBuilder.withBigText.build().frostConfig())
+
+ if (profileUrl.isNotBlank()) {
+ context.runOnUiThread {
+ Glide.with(context)
+ .asBitmap()
+ .load(profileUrl)
+ .withRoundIcon()
+ .into(FrostNotificationTarget(context, notifId, group, notifBuilder))
}
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
index fe7758cc..5859a306 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
@@ -1,26 +1,33 @@
package com.pitchedapps.frost.services
+import android.app.Notification
+import android.app.PendingIntent
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Context
+import android.content.Intent
+import android.net.Uri
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.activities.FrostWebActivity
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.dbflow.lastNotificationTime
import com.pitchedapps.frost.dbflow.loadFbCookie
import com.pitchedapps.frost.dbflow.loadFbCookiesSync
import com.pitchedapps.frost.facebook.FACEBOOK_COM
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.facebook.formattedFbUrl
+import com.pitchedapps.frost.injectors.JsAssets
+import com.pitchedapps.frost.utils.ARG_USER_ID
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostAnswersCustom
-import com.pitchedapps.frost.web.MessageWebView
+import com.pitchedapps.frost.web.launchHeadlessHtmlExtractor
+import io.reactivex.schedulers.Schedulers
import org.jetbrains.anko.doAsync
-import org.jetbrains.anko.uiThread
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.util.concurrent.Future
@@ -30,6 +37,9 @@ 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
*/
class NotificationService : JobService() {
@@ -58,7 +68,7 @@ class NotificationService : JobService() {
fun finish(params: JobParameters?) {
val time = System.currentTimeMillis() - startTime
- L.d("Notification service has finished in $time ms")
+ L.i("Notification service has finished in $time ms")
frostAnswersCustom("NotificationTime",
"Type" to "Service",
"IM Included" to Prefs.notificationsInstantMessages,
@@ -68,8 +78,8 @@ class NotificationService : JobService() {
future = null
}
-
override fun onStartJob(params: JobParameters?): Boolean {
+ L.i("Fetching notifications")
future = doAsync {
if (Prefs.notificationAllAccounts) {
val cookies = loadFbCookiesSync()
@@ -83,9 +93,15 @@ class NotificationService : JobService() {
L.d("Finished main notifications")
if (Prefs.notificationsInstantMessages) {
val currentCookie = loadFbCookie(Prefs.userId)
- if (currentCookie != null)
- uiThread { MessageWebView(this@NotificationService, params, currentCookie) }
- } else finish(params)
+ if (currentCookie != null) {
+ fetchMessageNotifications(currentCookie) {
+ L.i("Notif IM fetching finished ${if (it) "succesfully" else "unsuccessfully"}")
+ finish(params)
+ }
+ return@doAsync
+ }
+ }
+ finish(params)
}
return true
}
@@ -95,13 +111,21 @@ class NotificationService : JobService() {
return null
}
+ /*
+ * ----------------------------------------------------------------
+ * General notification logic.
+ * Fetch notifications -> Filter new ones -> Parse notifications ->
+ * Show notifications -> Show group notification
+ * ----------------------------------------------------------------
+ */
+
fun fetchGeneralNotifications(data: CookieModel) {
- L.i("Notif fetch for $data")
- val doc = Jsoup.connect(FbTab.NOTIFICATIONS.url).cookie(FACEBOOK_COM, data.cookie).userAgent(USER_AGENT_BASIC).get()
+ L.d("Notif fetch", data.toString())
+ val doc = Jsoup.connect(FbItem.NOTIFICATIONS.url).cookie(FACEBOOK_COM, data.cookie).userAgent(USER_AGENT_BASIC).get()
//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 prevLatestEpoch = 1498931565L // for testing
val prevNotifTime = lastNotificationTime(data.id)
val prevLatestEpoch = prevNotifTime.epoch
L.v("Notif Prev Latest Epoch $prevLatestEpoch")
@@ -122,7 +146,6 @@ class NotificationService : JobService() {
summaryNotification(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")
@@ -134,13 +157,36 @@ class NotificationService : JobService() {
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 ?: ""
+ val pUrl = profMatcher.find(p.attr("style"))?.groups?.get(1)?.value?.formattedFbUrl ?: ""
return NotificationContent(data, notifId.toInt(), a.attr("href"), null, text, epoch, pUrl)
}
- fun fetchMessageNotifications(data: CookieModel, content: String) {
- L.i("Notif IM fetch for $data")
- val doc = Jsoup.parseBodyFragment(content)
+ fun summaryNotification(userId: Long, count: Int)
+ = summaryNotification(userId, count, R.string.notifications, FbItem.NOTIFICATIONS.url, FROST_NOTIFICATION_GROUP)
+
+ /*
+ * ----------------------------------------------------------------
+ * Instant message notification logic.
+ * Fetch notifications -> Filter new ones -> Parse notifications ->
+ * Show notifications -> Show group notification
+ * ----------------------------------------------------------------
+ */
+
+ inline fun fetchMessageNotifications(data: CookieModel, crossinline callback: (success: Boolean) -> Unit) {
+ launchHeadlessHtmlExtractor(FbItem.MESSAGES.url, JsAssets.NOTIF_MSG) {
+ it.observeOn(Schedulers.newThread()).subscribe {
+ (html, errorRes) ->
+ L.d("Notf IM html received")
+ if (errorRes != -1) return@subscribe callback(false)
+ fetchMessageNotifications(data, html)
+ callback(true)
+ }
+ }
+ }
+
+ fun fetchMessageNotifications(data: CookieModel, html: String) {
+ L.d("Notif IM fetch", data.toString())
+ val doc = Jsoup.parseBodyFragment(html)
val unreadNotifications = (doc.getElementById("threadlist_rows") ?: return L.eThrow("Notification messages not found")).getElementsByClass("aclb")
var notifCount = 0
val prevNotifTime = lastNotificationTime(data.id)
@@ -152,7 +198,7 @@ class NotificationService : JobService() {
val notif = parseMessageNotification(data, elem) ?: return@unread
L.v("Notif im timestamp ${notif.timestamp}")
if (notif.timestamp <= prevLatestEpoch) return@unread
- notif.createNotification(this@NotificationService)
+ notif.createMessageNotification(this@NotificationService)
if (notif.timestamp > newLatestEpoch)
newLatestEpoch = notif.timestamp
notifCount++
@@ -160,7 +206,7 @@ class NotificationService : JobService() {
if (newLatestEpoch != prevLatestEpoch) prevNotifTime.copy(epochIm = newLatestEpoch).save()
L.d("Notif new latest im epoch ${lastNotificationTime(data.id).epochIm}")
frostAnswersCustom("Notifications", "Type" to "Message", "Count" to notifCount)
- summaryNotification(data.id, notifCount)
+ summaryMessageNotification(data.id, notifCount)
}
fun parseMessageNotification(data: CookieModel, element: Element): NotificationContent? {
@@ -174,11 +220,14 @@ class NotificationService : JobService() {
if (Prefs.notificationKeywords.any { text.contains(it, ignoreCase = true) }) return null //notification filtered out
//fetch convo pic
val p = element.select("i.img[style*=url]")
- val pUrl = profMatcher.find(p.attr("style"))?.groups?.get(1)?.value ?: ""
- L.v("url ${a.attr("href")}")
- return NotificationContent(data, notifId.toInt(), a.attr("href"), a.text(), text, epoch, pUrl.formattedFbUrl)
+ val pUrl = profMatcher.find(p.attr("style"))?.groups?.get(1)?.value?.formattedFbUrl ?: ""
+ L.v("url", a.attr("href"))
+ return NotificationContent(data, notifId.toInt(), a.attr("href"), a.text(), text, epoch, pUrl)
}
+ fun summaryMessageNotification(userId: Long, count: Int)
+ = summaryNotification(userId, count, R.string.messages, FbItem.MESSAGES.url, FROST_MESSAGE_NOTIFICATION_GROUP)
+
private fun Context.debugNotification(text: String) {
if (!BuildConfig.DEBUG) return
val notifBuilder = frostNotification
@@ -187,15 +236,21 @@ class NotificationService : JobService() {
NotificationManagerCompat.from(this).notify(999, notifBuilder.build().frostConfig())
}
- fun summaryNotification(userId: Long, count: Int) {
+ private fun summaryNotification(userId: Long, count: Int, contentRes: Int, pendingUrl: String, groupPrefix: String) {
if (count <= 1) return
+ val intent = Intent(this, FrostWebActivity::class.java)
+ intent.data = Uri.parse(pendingUrl)
+ intent.putExtra(ARG_USER_ID, userId)
+ val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val notifBuilder = frostNotification
.setContentTitle(string(R.string.frost_name))
- .setContentText("$count notifications")
- .setGroup("frost_$userId")
+ .setContentText("$count ${string(contentRes)}")
+ .setGroup("${groupPrefix}_$userId")
.setGroupSummary(true)
+ .setContentIntent(pendingIntent)
+ .setCategory(Notification.CATEGORY_SOCIAL)
- NotificationManagerCompat.from(this).notify("frost_$userId", userId.toInt(), notifBuilder.build().frostConfig())
+ NotificationManagerCompat.from(this).notify("${groupPrefix}_$userId", userId.toInt(), notifBuilder.build().frostConfig())
}
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt
index 52f7412f..9e53889e 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/UpdateReceiver.kt
@@ -13,6 +13,7 @@ import com.pitchedapps.frost.utils.Prefs
*/
class UpdateReceiver : BroadcastReceiver() {
+ //todo check action warning
override fun onReceive(context: Context, intent: Intent) {
L.d("Frost has updated")
context.scheduleNotifications(Prefs.notificationFreq) //Update notifications
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt
index 2af67602..bf524835 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt
@@ -31,6 +31,10 @@ fun SettingsActivity.getBehaviourPrefs(): KPrefAdapterBuilder.() -> Unit = {
descRes = R.string.search_bar_desc
}
+ checkbox(R.string.force_message_bottom, { Prefs.messageScrollToBottom }, { Prefs.messageScrollToBottom = it }) {
+ descRes = R.string.force_message_bottom_desc
+ }
+
checkbox(R.string.exit_confirmation, { Prefs.exitConfirmation }, { Prefs.exitConfirmation = it }) {
descRes = R.string.exit_confirmation_desc
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
new file mode 100644
index 00000000..f8dc81d1
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
@@ -0,0 +1,151 @@
+package com.pitchedapps.frost.settings
+
+import android.content.Context
+import android.support.annotation.UiThread
+import ca.allanwang.kau.email.sendEmail
+import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
+import ca.allanwang.kau.utils.string
+import com.afollestad.materialdialogs.MaterialDialog
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.activities.SettingsActivity
+import com.pitchedapps.frost.facebook.FACEBOOK_COM
+import com.pitchedapps.frost.facebook.FbCookie
+import com.pitchedapps.frost.facebook.FbItem
+import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.injectors.InjectorContract
+import com.pitchedapps.frost.injectors.JsActions
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.cleanHtml
+import com.pitchedapps.frost.utils.materialDialogThemed
+import com.pitchedapps.frost.web.launchHeadlessHtmlExtractor
+import com.pitchedapps.frost.web.query
+import io.reactivex.disposables.Disposable
+import org.jetbrains.anko.AnkoAsyncContext
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.runOnUiThread
+import org.jetbrains.anko.uiThread
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+
+/**
+ * Created by Allan Wang on 2017-06-30.
+ *
+ * A sub pref section that is enabled through a hidden preference
+ * Each category will load a page, extract the contents, remove private info, and create a report
+ */
+fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
+
+ plainText(R.string.experimental_disclaimer) {
+ descRes = R.string.debug_disclaimer_info
+ }
+
+ Debugger.values().forEach {
+ plainText(it.data.titleId) {
+ iicon = it.data.icon
+ onClick = { itemView, _, _ -> it.debug(itemView.context); true }
+ }
+ }
+
+}
+
+private enum class Debugger(val data: FbItem, val injector: InjectorContract?, vararg query: String) {
+ NOTIFICATIONS(FbItem.NOTIFICATIONS, null, "#notifications_list"),
+ SEARCH(FbItem.SEARCH, JsActions.FETCH_BODY);
+
+ val query = if (query.isNotEmpty()) arrayOf(*query, "#root", "main", "body") else emptyArray()
+
+ fun debug(context: Context) {
+ val dialog = context.materialDialogThemed {
+ title("Debugging")
+ progress(true, 0)
+ canceledOnTouchOutside(false)
+ positiveText(R.string.kau_cancel)
+ onPositive { dialog, _ -> dialog.cancel() }
+ }
+ if (injector != null) dialog.extractHtml(injector)
+ else dialog.debugAsync {
+ loadJsoup()
+ }
+ }
+
+ fun MaterialDialog.debugAsync(task: AnkoAsyncContext<MaterialDialog>.() -> Unit) {
+ doAsync({ t: Throwable ->
+ val msg = t.message
+ L.e("Debugger failed: $msg")
+ context.runOnUiThread {
+ cancel()
+ context.materialDialogThemed {
+ title(R.string.debug_incomplete)
+ if (msg != null) content(msg)
+ }
+ }
+ }, task)
+ }
+
+ /**
+ * Wait for html to be returned from headless webview
+ *
+ * from [debug] to [simplifyJsoup] if [query] is not empty, or [createReport] otherwise
+ */
+ @UiThread
+ private fun MaterialDialog.extractHtml(injector: InjectorContract) {
+ setContent("Fetching webpage")
+ var disposable: Disposable? = null
+ setOnCancelListener { disposable?.dispose() }
+ context.launchHeadlessHtmlExtractor(data.url, injector) {
+ disposable = it.subscribe {
+ (html, errorRes) ->
+ debugAsync {
+ if (errorRes == -1) {
+ L.i("Debug report successful", html)
+ if (query.isNotEmpty()) simplifyJsoup(Jsoup.parseBodyFragment(html))
+ else createReport(html)
+ } else {
+ throw Throwable(context.string(errorRes))
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Get data directly from the link and search for our queries, returning the outerHTML
+ * of the first query found
+ *
+ * from [debug] to [simplifyJsoup]
+ */
+ private fun AnkoAsyncContext<MaterialDialog>.loadJsoup() {
+ uiThread {
+ it.setContent("Load Jsoup")
+ it.setOnCancelListener(null)
+ it.debugAsync {
+ val connection = Jsoup.connect(data.url).cookie(FACEBOOK_COM, FbCookie.webCookie).userAgent(USER_AGENT_BASIC)
+ val doc = connection.get()
+ simplifyJsoup(doc)
+ }
+ }
+ }
+
+ /**
+ * Takes snippet of given document that matches the first query in the [query] items
+ * before sending it to [createReport]
+ */
+ private fun AnkoAsyncContext<MaterialDialog>.simplifyJsoup(doc: Document) {
+ weakRef.get() ?: return
+ val q = query.first { doc.select(it).isNotEmpty() }
+ createReport(doc.select(q).outerHtml())
+ }
+
+ private fun AnkoAsyncContext<MaterialDialog>.createReport(html: String) {
+ val cleanHtml = html.cleanHtml()
+ uiThread {
+ val c = it.context
+ it.dismiss()
+ c.sendEmail(c.string(R.string.dev_email),
+ "${c.string(R.string.debug_report_email_title)} $name") {
+ addItem("Query List", query.contentToString())
+ footer = cleanHtml
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt
index 594cbe01..a1b459fb 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Experimental.kt
@@ -1,9 +1,11 @@
package com.pitchedapps.frost.settings
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
+import ca.allanwang.kau.logging.KL
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SettingsActivity
+import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.Showcase
@@ -22,13 +24,17 @@ fun SettingsActivity.getExperimentalPrefs(): KPrefAdapterBuilder.() -> Unit = {
// Experimental content starts here ------------------
- checkbox(R.string.notification_messages, { Prefs.notificationsInstantMessages }, { Prefs.notificationsInstantMessages = it }) {
- descRes = R.string.notification_messages_desc
- }
+
// Experimental content ends here --------------------
- checkbox(R.string.verbose_logging, { Prefs.verboseLogging }, { Prefs.verboseLogging = it }) {
+ checkbox(R.string.verbose_logging, { Prefs.verboseLogging }, {
+ Prefs.verboseLogging = it
+ KL.debug(it)
+ KL.showPrivateText = false
+ L.debug(it)
+ KL.showPrivateText = false
+ }) {
descRes = R.string.verbose_logging_desc
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Network.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Network.kt
new file mode 100644
index 00000000..30ab2579
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Network.kt
@@ -0,0 +1,17 @@
+package com.pitchedapps.frost.settings
+
+import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.activities.SettingsActivity
+import com.pitchedapps.frost.utils.Prefs
+
+/**
+ * Created by Allan Wang on 2017-08-08.
+ */
+fun SettingsActivity.getNetworkPrefs(): KPrefAdapterBuilder.() -> Unit = {
+
+ checkbox(R.string.network_media_on_metered, { Prefs.loadMediaOnMeteredNetwork }, { Prefs.loadMediaOnMeteredNetwork = it }) {
+ descRes = R.string.network_media_on_metered_desc
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt
index a5aa84d3..b1e5015f 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt
@@ -55,6 +55,10 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = {
descRes = R.string.notification_all_accounts_desc
}
+ checkbox(R.string.notification_messages, { Prefs.notificationsInstantMessages }, { Prefs.notificationsInstantMessages = it }) {
+ descRes = R.string.notification_messages_desc
+ }
+
checkbox(R.string.notification_sound, { Prefs.notificationSound }, { Prefs.notificationSound = it })
checkbox(R.string.notification_vibrate, { Prefs.notificationVibrate }, { Prefs.notificationVibrate = it })
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/JsoupCleaner.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/JsoupCleaner.kt
new file mode 100644
index 00000000..da8672f4
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/JsoupCleaner.kt
@@ -0,0 +1,34 @@
+package com.pitchedapps.frost.utils
+
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Attribute
+import org.jsoup.nodes.Element
+import org.jsoup.safety.Whitelist
+
+/**
+ * Created by Allan Wang on 2017-08-10.
+ *
+ * Parses html with Jsoup and cleans the data, emitting just the frame containing debugging info
+ *
+ * Removes text, removes unnecessary nodes
+ */
+fun String.cleanHtml() = cleanText().cleanJsoup()
+
+internal fun String.cleanText(): String = replace(Regex(">(?s).+?<"), "><")
+
+internal fun String.cleanJsoup(): String = Jsoup.clean(this, PrivacyWhitelist())
+
+class PrivacyWhitelist : Whitelist() {
+
+ val blacklistAttrs = arrayOf("style", "aria-label", "rel")
+ val blacklistTags = arrayOf("body", "html", "head", "i", "b", "u", "style", "script",
+ "br", "p", "span", "ul", "ol", "li")
+
+ override fun isSafeAttribute(tagName: String, el: Element, attr: Attribute): Boolean {
+ val key = attr.key
+ if (key == "href") attr.setValue("-")
+ return key !in blacklistAttrs
+ }
+
+ override fun isSafeTag(tag: String) = tag !in blacklistTags
+}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt
index 16a3d2ae..d5c1a6fb 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt
@@ -1,9 +1,8 @@
package com.pitchedapps.frost.utils
-import android.util.Log
-import ca.allanwang.kau.logging.TimberLogger
+import ca.allanwang.kau.logging.KauLogger
import com.crashlytics.android.Crashlytics
-import timber.log.Timber
+import com.pitchedapps.frost.BuildConfig
/**
@@ -16,25 +15,15 @@ import timber.log.Timber
* Debug and Error logs must not reveal person info
* Person info logs can be marked as info or verbose
*/
-object L : TimberLogger("Frost") {
+object L : KauLogger("Frost") {
- /**
- * Helper function to separate private info
- */
- fun d(tag: String, personal: String?) {
- L.d(tag)
- L.i("-\t$personal")
- }
-}
-
-internal class CrashReportingTree : Timber.Tree() {
- override fun log(priority: Int, tag: String?, message: String?, t: Throwable?) {
- when (priority) {
- Log.VERBOSE, Log.INFO -> return
- Log.DEBUG -> if (!Prefs.verboseLogging) return
+ override fun logImpl(priority: Int, message: String?, privateMessage: String?, t: Throwable?) {
+ if (BuildConfig.DEBUG) {
+ super.logImpl(priority, message, privateMessage, t)
+ } else {
+ if (message != null)
+ Crashlytics.log(priority, "Frost", message)
+ if (t != null) Crashlytics.logException(t)
}
- if (message != null)
- Crashlytics.log(priority, "Frost", message)
- if (t != null) Crashlytics.logException(t)
}
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
index b053b9dd..9b8064a4 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
@@ -97,8 +97,7 @@ object Prefs : KPref() {
var notificationAllAccounts: Boolean by kpref("notification_all_accounts", true)
- //todo remove from experimental once stabilized
- var notificationsInstantMessages: Boolean by kpref("notification_im", Showcase.experimentalDefault)
+ var notificationsInstantMessages: Boolean by kpref("notification_im", false)
var notificationVibrate: Boolean by kpref("notification_vibrate", true)
@@ -106,6 +105,8 @@ object Prefs : KPref() {
var notificationLights: Boolean by kpref("notification_lights", true)
+ var messageScrollToBottom: Boolean by kpref("message_scroll_to_bottom", false)
+
/**
* Cache like value to determine if user has or had pro
* In most cases, [com.pitchedapps.frost.utils.iab.IS_FROST_PRO] should be looked at instead
@@ -128,4 +129,8 @@ object Prefs : KPref() {
var viewpagerSwipe: Boolean by kpref("viewpager_swipe", true)
+ var loadMediaOnMeteredNetwork: Boolean by kpref("media_on_metered_network", true)
+
+ var debugSettings: Boolean by kpref("debug_settings", false)
+
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
index 496a6b5b..e79816f3 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
@@ -25,7 +25,7 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.*
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.FACEBOOK_COM
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.formattedFbUrl
import java.io.IOException
import java.util.*
@@ -56,8 +56,8 @@ fun Activity.cookies(): ArrayList<CookieModel> {
fun Context.launchWebOverlay(url: String) {
val argUrl = url.formattedFbUrl
- L.v("Launch received $url")
- L.i("Launch web overlay: $argUrl")
+ L.v("Launch received", url)
+ L.i("Launch web overlay", argUrl)
startActivity(WebOverlayActivity::class.java, false, intentBuilder = {
putExtra(ARG_URL, argUrl)
})
@@ -74,7 +74,7 @@ fun Activity.launchIntroActivity(cookieList: ArrayList<CookieModel>)
= launchNewTask(IntroActivity::class.java, cookieList, true)
fun WebOverlayActivity.url(): String {
- return intent.extras?.getString(ARG_URL) ?: FbTab.FEED.url
+ return intent.extras?.getString(ARG_URL) ?: FbItem.FEED.url
}
fun Context.materialDialogThemed(action: MaterialDialog.Builder.() -> Unit): MaterialDialog {
@@ -132,6 +132,15 @@ fun frostAnswersCustom(name: String, vararg events: Pair<String, Any>) {
}
}
+/**
+ * Helper method to quietly keep track of throwable issues
+ */
+fun Throwable?.logFrostAnswers(text: String) {
+ val msg = if (this == null) text else "$text: $message"
+ L.e(msg)
+ frostAnswersCustom("Errors", "text" to text, "message" to (this?.message ?: "NA"))
+}
+
fun View.frostSnackbar(@StringRes text: Int, builder: Snackbar.() -> Unit = {}) {
Snackbar.make(this, text, Snackbar.LENGTH_LONG).apply {
builder()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt
index bad7f8fd..7f6e8a6d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABBinder.kt
@@ -9,6 +9,13 @@ import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostAnswers
+import com.pitchedapps.frost.utils.logFrostAnswers
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.onComplete
+import org.jetbrains.anko.uiThread
+import java.lang.ref.WeakReference
+import java.math.BigDecimal
+import java.util.*
/**
* Created by Allan Wang on 2017-07-22.
@@ -33,31 +40,48 @@ interface FrostBilling : BillingProcessor.IBillingHandler {
abstract class IABBinder : FrostBilling {
var bp: BillingProcessor? = null
- var activity: Activity? = null
-
- override fun Activity.onCreateBilling() {
- activity = this
- bp = BillingProcessor.newBillingProcessor(this, PUBLIC_BILLING_KEY, this@IABBinder)
- bp?.initialize()
+ lateinit var activityRef: WeakReference<Activity>
+ val activity
+ get() = activityRef.get()
+
+ override final fun Activity.onCreateBilling() {
+ activityRef = WeakReference(this)
+ doAsync {
+ bp = BillingProcessor.newBillingProcessor(this@onCreateBilling, PUBLIC_BILLING_KEY, this@IABBinder)
+ bp?.initialize()
+ }
}
override fun onDestroyBilling() {
+ activityRef.clear()
bp?.release()
bp = null
- activity = null
}
- override fun onBillingInitialized() = L.d("IAB initialized")
+ override fun onBillingInitialized() = L.i("IAB initialized")
override fun onPurchaseHistoryRestored() = L.d("IAB restored")
override fun onProductPurchased(productId: String, details: TransactionDetails?) {
- L.d("IAB $productId purchased")
- frostAnswers {
- logPurchase(PurchaseEvent()
- .putItemId(productId)
- .putSuccess(true)
- )
+ bp.doAsync {
+ L.i("IAB $productId purchased")
+ val listing = weakRef.get()?.getPurchaseListingDetails(productId) ?: return@doAsync
+ val currency = try {
+ Currency.getInstance(listing.currency)
+ } catch (e: Exception) {
+ null
+ }
+ frostAnswers {
+ logPurchase(PurchaseEvent().apply {
+ putItemId(productId)
+ putSuccess(true)
+ if (currency != null) {
+ putCurrency(Currency.getInstance(Locale.getDefault()))
+ putItemType(productId)
+ putItemPrice(BigDecimal.valueOf(listing.priceValue))
+ }
+ })
+ }
}
}
@@ -67,13 +91,14 @@ abstract class IABBinder : FrostBilling {
.putCustomAttribute("result", errorCode.toString())
.putSuccess(false))
}
- L.e(error, "IAB error $errorCode")
+ error.logFrostAnswers("IAB error $errorCode")
}
override fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean
= bp?.handleActivityResult(requestCode, resultCode, data) ?: false
override fun purchasePro() {
+ val bp = this.bp
if (bp == null) {
frostAnswers {
logPurchase(PurchaseEvent()
@@ -83,10 +108,12 @@ abstract class IABBinder : FrostBilling {
L.eThrow("IAB null bp on purchase attempt")
return
}
- if (!(bp?.isOneTimePurchaseSupported ?: false))
- activity?.playStorePurchaseUnsupported()
+ val a = activity ?: return
+
+ if (!BillingProcessor.isIabServiceAvailable(a) || !bp.isOneTimePurchaseSupported)
+ a.playStorePurchaseUnsupported()
else
- bp?.purchase(activity, FROST_PRO)
+ bp.purchase(a, FROST_PRO)
}
}
@@ -107,15 +134,18 @@ class IABSettings : IABBinder() {
* Attempts to get pro, or launch purchase flow if user doesn't have it
*/
override fun restorePurchases() {
- if (bp == null) return
- val load = bp?.loadOwnedPurchasesFromGoogle() ?: return
- L.d("IAB settings load from google $load")
- if (!(bp?.isPurchased(FROST_PRO) ?: return)) {
- if (Prefs.pro) activity.playStoreNoLongerPro()
- else purchasePro()
- } else {
- if (!Prefs.pro) activity.playStoreFoundPro()
- else activity?.purchaseRestored()
+ bp.doAsync {
+ val load = weakRef.get()?.loadOwnedPurchasesFromGoogle() ?: return@doAsync
+ L.d("IAB settings load from google $load")
+ uiThread {
+ if (!(weakRef.get()?.isPurchased(FROST_PRO) ?: return@uiThread)) {
+ if (Prefs.pro) activity.playStoreNoLongerPro()
+ else purchasePro()
+ } else {
+ if (!Prefs.pro) activity.playStoreFoundPro()
+ else activity?.purchaseRestored()
+ }
+ }
}
}
}
@@ -142,13 +172,17 @@ class IABMain : IABBinder() {
override fun restorePurchases() {
if (restored || bp == null) return
restored = true
- val load = bp?.loadOwnedPurchasesFromGoogle() ?: false
- L.d("IAB main load from google $load")
- if (!(bp?.isPurchased(FROST_PRO) ?: false)) {
- if (Prefs.pro) activity.playStoreNoLongerPro()
- } else {
- if (!Prefs.pro) activity.playStoreFoundPro()
+ bp.doAsync {
+ val load = weakRef.get()?.loadOwnedPurchasesFromGoogle() ?: false
+ L.d("IAB main load from google $load")
+ onComplete {
+ if (!(weakRef.get()?.isPurchased(FROST_PRO) ?: false)) {
+ if (Prefs.pro) activity.playStoreNoLongerPro()
+ } else {
+ if (!Prefs.pro) activity.playStoreFoundPro()
+ }
+ onDestroyBilling()
+ }
}
- onDestroyBilling()
}
} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt
index df0f04fd..e997731b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IABDialogs.kt
@@ -2,6 +2,7 @@ package com.pitchedapps.frost.utils.iab
import android.app.Activity
import ca.allanwang.kau.utils.restart
+import ca.allanwang.kau.utils.startLink
import ca.allanwang.kau.utils.startPlayStoreLink
import ca.allanwang.kau.utils.string
import com.crashlytics.android.answers.PurchaseEvent
@@ -69,9 +70,11 @@ fun Activity.playStorePurchaseUnsupported() {
materialDialogThemed {
title(R.string.uh_oh)
content(R.string.play_store_unsupported)
- positiveText(R.string.kau_ok)
- neutralText(R.string.kau_play_store)
- onNeutral { _, _ -> startPlayStoreLink(R.string.play_store_package_id) }
+ negativeText(R.string.kau_close)
+ positiveText(R.string.kau_play_store)
+ neutralText(R.string.paypal)
+ onPositive { _, _ -> startPlayStoreLink(R.string.play_store_package_id) }
+ onNeutral { _, _ -> startLink(string(R.string.dev_paypal)) }
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
index 6bc27256..61711092 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
@@ -34,15 +34,9 @@ class FrostChromeClient(webCore: FrostWebViewCore) : WebChromeClient() {
val activityContract = (webCore.context as? ActivityWebContract)
val context = webCore.context!!
- companion object {
- val consoleBlacklist = setOf(
- "edge-chat"
- )
- }
-
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
if (consoleBlacklist.any { consoleMessage.message().contains(it) }) return true
- L.i("Chrome Console ${consoleMessage.lineNumber()}: ${consoleMessage.message()}")
+ L.d("Chrome Console ${consoleMessage.lineNumber()}: ${consoleMessage.message()}")
return true
}
@@ -63,10 +57,10 @@ class FrostChromeClient(webCore: FrostWebViewCore) : WebChromeClient() {
}
override fun onGeolocationPermissionsShowPrompt(origin: String, callback: GeolocationPermissions.Callback) {
- L.d("Requesting geolocation")
+ L.i("Requesting geolocation")
context.kauRequestPermissions(PERMISSION_ACCESS_FINE_LOCATION) {
granted, _ ->
- L.d("Geolocation response received; ${if (granted) "granted" else "denied"}")
+ L.i("Geolocation response received; ${if (granted) "granted" else "denied"}")
callback(origin, granted, true)
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
index 018ad737..f24a7a51 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
@@ -81,12 +81,14 @@ class FrostJSI(val webView: FrostWebViewCore) {
}
@JavascriptInterface
- fun handleHtml(html: String) {
+ fun handleHtml(html: String?) {
+ html ?: return
webView.post { webView.frostWebClient.handleHtml(html) }
}
@JavascriptInterface
- fun handleHeader(html: String) {
+ fun handleHeader(html: String?) {
+ html ?: return
headerObservable?.onNext(html)
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt
index 3f2891d0..1a907f7f 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt
@@ -5,6 +5,7 @@ import android.webkit.WebResourceResponse
import android.webkit.WebView
import ca.allanwang.kau.utils.use
import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.Prefs
import okhttp3.HttpUrl
import java.io.ByteArrayInputStream
@@ -15,17 +16,17 @@ import java.io.ByteArrayInputStream
* Handler to decide when a request should be done by us
* This is the crux of Frost's optimizations for the web browser
*/
-val blankResource: WebResourceResponse by lazy { WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray())) }
+private val blankResource: WebResourceResponse by lazy { WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray())) }
//these hosts will redirect to a blank resource
-val blacklistHost: Set<String> by lazy {
+private val blacklistHost: Set<String> by lazy {
setOf(
"edge-chat.facebook.com"
)
}
//these hosts will return null and skip logging
-val whitelistHost: Set<String> by lazy {
+private val whitelistHost: Set<String> by lazy {
setOf(
"static.xx.fbcdn.net",
"m.facebook.com",
@@ -35,13 +36,13 @@ val whitelistHost: Set<String> by lazy {
//these hosts will skip ad inspection
//this list does not have to include anything from the two above
-val adWhitelistHost: Set<String> by lazy {
+private val adWhitelistHost: Set<String> by lazy {
setOf(
"scontent-sea1-1.xx.fbcdn.net"
)
}
-var adblock: Set<String>? = null
+private var adblock: Set<String>? = null
fun shouldFrostInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
val httpUrl = HttpUrl.parse(request.url?.toString() ?: return null) ?: return null
@@ -53,7 +54,8 @@ fun shouldFrostInterceptRequest(view: WebView, request: WebResourceRequest): Web
if (adblock == null) adblock = view.context.assets.open("adblock.txt").bufferedReader().use { it.readLines().toSet() }
if (adblock?.any { url.contains(it) } ?: false) return blankResource
}
- L.v("Intercept Request ${host} ${url}")
+ if (!shouldLoadImages && !Prefs.loadMediaOnMeteredNetwork && request.isMedia) return blankResource
+ L.v("Intercept Request", "$host $url")
return null
}
@@ -64,16 +66,25 @@ fun WebResourceRequest.query(action: (url: String) -> Boolean): Boolean {
return action(url?.path ?: return false)
}
+val WebResourceRequest.isImage: Boolean
+ get() = query { it.contains(".jpg") || it.contains(".png") }
+
+val WebResourceRequest.isMedia: Boolean
+ get() = query { it.contains(".jpg") || it.contains(".png") || it.contains("video") }
+
/**
* Generic filter passthrough
* If Resource is already nonnull, pass it, otherwise check if filter is met and override the response accordingly
*/
-fun WebResourceResponse?.filter(request: WebResourceRequest, filter: (url: String) -> Boolean): WebResourceResponse?
- = this ?: if (request.query { filter(it) }) blankResource else null
+fun WebResourceResponse?.filter(request: WebResourceRequest, filter: (url: String) -> Boolean)
+ = filter(request.query { filter(it) })
+
+fun WebResourceResponse?.filter(filter: Boolean): WebResourceResponse?
+ = this ?: if (filter) blankResource else null
fun WebResourceResponse?.filterCss(request: WebResourceRequest): WebResourceResponse?
= filter(request) { it.endsWith(".css") }
fun WebResourceResponse?.filterImage(request: WebResourceRequest): WebResourceResponse?
- = filter(request) { it.contains(".jpg") || it.contains(".png") }
+ = filter(request.isImage)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt
index 79ca1fdf..89ad766d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebView.kt
@@ -11,7 +11,7 @@ import android.widget.FrameLayout
import android.widget.ProgressBar
import ca.allanwang.kau.utils.*
import com.pitchedapps.frost.R
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostDownload
@@ -53,7 +53,7 @@ class FrostWebView @JvmOverloads constructor(
}
@SuppressLint("SetJavaScriptEnabled")
- fun setupWebview(url: String, enum: FbTab? = null) {
+ fun setupWebview(url: String, enum: FbItem? = null) {
with(web) {
baseUrl = url
baseEnum = enum
@@ -77,7 +77,7 @@ class FrostWebView @JvmOverloads constructor(
//Some urls have postJavascript injections so make sure we load the base url
override fun onRefresh() {
when (web.baseUrl) {
- FbTab.MENU.url -> web.loadBaseUrl(true)
+ FbItem.MENU.url -> web.loadBaseUrl(true)
else -> web.reload(true)
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt
index 94bff3c3..5f679c65 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt
@@ -1,8 +1,8 @@
package com.pitchedapps.frost.web
import android.content.Context
-import android.content.Intent
import android.graphics.Bitmap
+import android.graphics.Color
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
@@ -14,10 +14,12 @@ import com.pitchedapps.frost.activities.WebOverlayActivity
import com.pitchedapps.frost.facebook.FACEBOOK_COM
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FbCookie
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.injectors.*
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import io.reactivex.subjects.Subject
+import org.jetbrains.anko.withAlpha
/**
* Created by Allan Wang on 2017-05-31.
@@ -42,18 +44,19 @@ open class BaseWebViewClient : WebViewClient() {
open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient() {
val refreshObservable: Subject<Boolean> = webCore.refreshObservable
+ val isMain = webCore.baseEnum != null
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
if (url == null) return
- L.i("FWV Loading $url")
-// L.v("Cookies ${CookieManager.getInstance().getCookie(url)}")
+ L.i("FWV Loading", url)
refreshObservable.onNext(true)
if (!url.contains(FACEBOOK_COM)) return
if (url.contains("logout.php")) FbCookie.logout(Prefs.userId, { launchLogin(view.context) })
else if (url.contains("login.php")) FbCookie.reset({ launchLogin(view.context) })
}
+
fun launchLogin(c: Context) {
if (c is MainActivity && c.cookies().isNotEmpty())
c.launchNewTask(SelectorActivity::class.java, c.cookies())
@@ -61,44 +64,52 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
c.launchNewTask(LoginActivity::class.java)
}
+ fun injectBackgroundColor()
+ = webCore.setBackgroundColor(if (isMain) Color.TRANSPARENT else Prefs.bgColor.withAlpha(255))
+
+
+ override fun onPageCommitVisible(view: WebView, url: String?) {
+ super.onPageCommitVisible(view, url)
+ injectBackgroundColor()
+ view.jsInject(
+ CssAssets.ROUND_ICONS.maybe(Prefs.showRoundedIcons),
+ CssHider.HEADER,
+ CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!Prefs.showSuggestedFriends && IS_FROST_PRO),
+ Prefs.themeInjector,
+ CssHider.NON_RECENT.maybe(webCore.url?.contains("?sk=h_chr") ?: false))
+ }
+
override fun onPageFinished(view: WebView, url: String?) {
- super.onPageFinished(view, url)
- if (url == null) return
- L.i("Page finished $url")
+ url ?: return
+ L.i("Page finished", url)
if (!url.contains(FACEBOOK_COM)) {
refreshObservable.onNext(false)
return
}
- view.jsInject(
- CssAssets.ROUND_ICONS.maybe(Prefs.showRoundedIcons),
- CssHider.PEOPLE_YOU_MAY_KNOW.maybe(!Prefs.showSuggestedFriends && IS_FROST_PRO),
- CssHider.ADS.maybe(!Prefs.showFacebookAds && IS_FROST_PRO)
- )
onPageFinishedActions(url)
}
open internal fun onPageFinishedActions(url: String) {
+ if (url.startsWith("${FbItem.MESSAGES.url}/read/") && Prefs.messageScrollToBottom)
+ webCore.pageDown(true)
injectAndFinish()
}
internal fun injectAndFinish() {
L.d("Page finished reveal")
- webCore.jsInject(CssHider.HEADER,
- CssHider.NON_RECENT.maybe(webCore.url.contains("?sk=h_chr")),
- Prefs.themeInjector,
- callback = {
- refreshObservable.onNext(false)
- webCore.jsInject(
- JsActions.LOGIN_CHECK,
- JsAssets.CLICK_A.maybe(webCore.baseEnum != null && Prefs.overlayEnabled),
- JsAssets.TEXTAREA_LISTENER,
- JsAssets.CONTEXT_A,
- JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null)
- )
- })
- }
-
- open fun handleHtml(html: String) {
+ refreshObservable.onNext(false)
+ injectBackgroundColor()
+ webCore.jsInject(
+ JsActions.LOGIN_CHECK,
+ JsAssets.CLICK_A.maybe(webCore.baseEnum != null && Prefs.overlayEnabled),
+ JsAssets.TEXTAREA_LISTENER,
+ CssHider.ADS.maybe(!Prefs.showFacebookAds && IS_FROST_PRO),
+ JsAssets.CONTEXT_A,
+ JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null)
+ )
+ }
+
+ open fun handleHtml(html: String?) {
L.d("Handle Html")
}
@@ -112,26 +123,26 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient
* returns false if we are already in an overlaying activity
*/
private fun launchRequest(request: WebResourceRequest): Boolean {
- L.d("Launching Url", request.url.toString())
+ L.d("Launching Url", request.url?.toString() ?: "null")
if (webCore.context is WebOverlayActivity) return false
webCore.context.launchWebOverlay(request.url.toString())
return true
}
- private fun launchImage(request: WebResourceRequest, text: String? = null): Boolean {
- L.d("Launching Image", request.url.toString())
- webCore.context.launchImageActivity(request.url.toString(), text)
+ private fun launchImage(url: String, text: String? = null): Boolean {
+ L.d("Launching Image", url)
+ webCore.context.launchImageActivity(url, text)
if (webCore.canGoBack()) webCore.goBack()
return true
}
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
- L.i("Url Loading ${request.url}")
- val path = request.url.path ?: return super.shouldOverrideUrlLoading(view, request)
- L.v("Url Loading Path $path")
+ L.i("Url Loading", request.url?.toString())
+ val path = request.url?.path ?: return super.shouldOverrideUrlLoading(view, request)
+ L.v("Url Loading Path", path)
if (path.startsWith("/composer/")) return launchRequest(request)
if (request.url.toString().contains("scontent-sea1-1.xx.fbcdn.net") && (path.endsWith(".jpg") || path.endsWith(".png")))
- return launchImage(request)
+ return launchImage(request.url.toString())
if (view.context.resolveActivityForUri(request.url)) return true
return super.shouldOverrideUrlLoading(view, request)
}
@@ -163,6 +174,7 @@ class FrostWebViewClientMenu(webCore: FrostWebViewCore) : FrostWebViewClient(web
}
override fun onPageFinishedActions(url: String) {
+ L.d("Should inject ${url.shouldInjectMenu}")
if (!url.shouldInjectMenu) injectAndFinish()
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt
index d8edc15c..6dbc7c8d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewCore.kt
@@ -14,7 +14,7 @@ import ca.allanwang.kau.utils.circularReveal
import ca.allanwang.kau.utils.fadeIn
import ca.allanwang.kau.utils.fadeOut
import ca.allanwang.kau.utils.isVisible
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.utils.Prefs
import io.reactivex.Scheduler
import io.reactivex.android.schedulers.AndroidSchedulers
@@ -41,7 +41,7 @@ class FrostWebViewCore @JvmOverloads constructor(
var baseUrl: String? = null
- var baseEnum: FbTab? = null //only viewpager items should pass the base enum
+ var baseEnum: FbItem? = null //only viewpager items should pass the base enum
internal lateinit var frostWebClient: FrostWebViewClient
init {
@@ -76,7 +76,7 @@ class FrostWebViewCore @JvmOverloads constructor(
if (isVisible) fadeOut(duration = 200L)
} else if (loading) {
dispose?.dispose()
- if (animate && Prefs.animate) circularReveal(offset = 150L)
+ if (animate && Prefs.animate) circularReveal(offset = WEB_LOAD_DELAY)
else fadeIn(duration = 100L)
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/HeadlessHtmlExtractor.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/HeadlessHtmlExtractor.kt
new file mode 100644
index 00000000..50f2f6bc
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/HeadlessHtmlExtractor.kt
@@ -0,0 +1,88 @@
+package com.pitchedapps.frost.web
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import ca.allanwang.kau.utils.gone
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.injectors.InjectorContract
+import com.pitchedapps.frost.utils.L
+import io.reactivex.Single
+import io.reactivex.SingleEmitter
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
+import org.jetbrains.anko.runOnUiThread
+import java.util.concurrent.TimeUnit
+
+/**
+ * Created by Allan Wang on 2017-08-12.
+ *
+ * Launches a headless html request and returns a result pair
+ * When successful, the pair will contain the html content and -1
+ * When unsuccessful, the pair will contain an empty string and a StringRes for the given error
+ *
+ * All errors are rerouted to success calls, so no exceptions should occur.
+ * The headless extractor will also destroy itself on cancellation or when the request is finished
+ */
+fun Context.launchHeadlessHtmlExtractor(url: String, injector: InjectorContract, action: (Single<Pair<String, Int>>) -> Unit) {
+ val single = Single.create<Pair<String, Int>> { e: SingleEmitter<Pair<String, Int>> ->
+ val extractor = HeadlessHtmlExtractor(this, url, injector, e)
+ e.setCancellable {
+ runOnUiThread { extractor.destroy() }
+ e.onSuccess("" to R.string.html_extraction_cancelled)
+ }
+ }.subscribeOn(AndroidSchedulers.mainThread())
+ .timeout(20, TimeUnit.SECONDS, Schedulers.io(), { it.onSuccess("" to R.string.html_extraction_timeout) })
+ .onErrorReturn { "" to R.string.html_extraction_error }
+ action(single)
+}
+
+/**
+ * Given a link and some javascript, will load the link and load the JS on completion
+ * The JS is expected to call [HeadlessHtmlExtractor.HtmlJSI.handleHtml], which will be sent
+ * to the [emitter]
+ */
+@SuppressLint("ViewConstructor")
+private class HeadlessHtmlExtractor(
+ context: Context, url: String, val injector: InjectorContract, val emitter: SingleEmitter<Pair<String, Int>>
+) : WebView(context) {
+
+ val startTime = System.currentTimeMillis()
+
+ init {
+ L.v("Created HeadlessHtmlExtractor for $url")
+ gone()
+ setupWebview(url)
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun setupWebview(url: String) {
+ settings.javaScriptEnabled = true
+ settings.userAgentString = USER_AGENT_BASIC
+ webViewClient = HeadlessWebViewClient(url, injector) // basic client that loads our JS once the page has loaded
+ webChromeClient = QuietChromeClient() // basic client that disables logging
+ addJavascriptInterface(HtmlJSI(), "Frost")
+ loadUrl(url)
+ }
+
+ inner class HtmlJSI {
+ @JavascriptInterface
+ fun handleHtml(html: String?) {
+ val time = System.currentTimeMillis() - startTime
+ emitter.onSuccess((html ?: "") to -1)
+ post {
+ L.d("HeadlessHtmlExtractor fetched $url in $time ms")
+ settings.javaScriptEnabled = false
+ settings.blockNetworkLoads = true
+ destroy()
+ }
+ }
+ }
+
+ override fun destroy() {
+ super.destroy()
+ L.d("HeadlessHtmlExtractor destroyed")
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
index 31be4450..aea25337 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
@@ -60,7 +60,7 @@ class LoginWebView @JvmOverloads constructor(
view.jsInject(CssHider.HEADER.maybe(containsFacebook),
CssHider.CORE.maybe(containsFacebook),
Prefs.themeInjector.maybe(containsFacebook),
- callback = { if (!view.isVisible) view.fadeIn(offset = 150L) })
+ callback = { if (!view.isVisible) view.fadeIn(offset = WEB_LOAD_DELAY) })
}
fun checkForLogin(url: String?, onFound: (id: Long, cookie: String) -> Unit) {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/MessageWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/MessageWebView.kt
deleted file mode 100644
index 53fa0657..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/MessageWebView.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.pitchedapps.frost.web
-
-import android.annotation.SuppressLint
-import android.app.job.JobParameters
-import android.webkit.JavascriptInterface
-import android.webkit.WebView
-import ca.allanwang.kau.utils.gone
-import com.pitchedapps.frost.dbflow.CookieModel
-import com.pitchedapps.frost.facebook.FbTab
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
-import com.pitchedapps.frost.injectors.JsAssets
-import com.pitchedapps.frost.services.NotificationService
-import com.pitchedapps.frost.utils.L
-import com.pitchedapps.frost.utils.frostAnswersCustom
-import org.jetbrains.anko.doAsync
-
-/**
- * Created by Allan Wang on 2017-07-17.
- *
- * Bare boned headless view made solely to extract conversation info
- */
-@SuppressLint("ViewConstructor")
-class MessageWebView(val service: NotificationService, val params: JobParameters?, val cookie: CookieModel) : WebView(service) {
-
- private val startTime = System.currentTimeMillis()
- private var isCancelled = false
-
- init {
- gone()
- setupWebview()
- }
-
- @SuppressLint("SetJavaScriptEnabled")
- private fun setupWebview() {
- settings.javaScriptEnabled = true
- settings.userAgentString = USER_AGENT_BASIC
- webViewClient = HeadlessWebViewClient("MessageNotifs", JsAssets.NOTIF_MSG)
- webChromeClient = QuietChromeClient()
- addJavascriptInterface(MessageJSI(), "Frost")
- loadUrl(FbTab.MESSAGES.url)
- }
-
- fun finish() {
- if (isCancelled) return
- isCancelled = true
- post { destroy() }
- service.finish(params)
- }
-
- override fun destroy() {
- L.d("MessageWebView destroyed")
- super.destroy()
- }
-
- inner class MessageJSI {
- @JavascriptInterface
- fun handleHtml(html: String) {
- if (isCancelled) return
- if (html.length < 10) return finish()
- val time = System.currentTimeMillis() - startTime
- L.d("Notif messages fetched in $time ms")
- frostAnswersCustom("NotificationTime", "Type" to "IM Headless", "Duration" to time)
- doAsync { service.fetchMessageNotifications(cookie, html); finish() }
- }
- }
-
-} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt
index 05d56f92..da6d8ad3 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/SearchWebView.kt
@@ -6,7 +6,7 @@ import android.webkit.JavascriptInterface
import android.webkit.WebView
import ca.allanwang.kau.searchview.SearchItem
import ca.allanwang.kau.utils.gone
-import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.injectors.JsAssets
import com.pitchedapps.frost.injectors.JsBuilder
@@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit
*/
class SearchWebView(context: Context, val contract: SearchContract) : WebView(context) {
- val searchSubject = PublishSubject.create<String>()
+ val searchSubject = PublishSubject.create<String>()!!
init {
gone()
@@ -39,11 +39,11 @@ class SearchWebView(context: Context, val contract: SearchContract) : WebView(co
* Contains the last item's href (search more) as well as the number of items found
* This holder is synchronized
*/
- var previousResult: Pair<String?, Int> = Pair(null, 0)
+ var previousResult: Pair<String, Int> = Pair("", 0)
fun saveResultFrame(result: List<Pair<List<String>, String>>) {
synchronized(previousResult) {
- previousResult = Pair(result.lastOrNull()?.second, result.size)
+ previousResult = Pair(result.last().second, result.size)
}
}
@@ -56,17 +56,22 @@ class SearchWebView(context: Context, val contract: SearchContract) : WebView(co
addJavascriptInterface(SearchJSI(), "Frost")
searchSubject.debounce(300, TimeUnit.MILLISECONDS).subscribeOn(Schedulers.newThread())
.map {
- Jsoup.parse(it).select("a:not([rel*='keywords(']):not([href=#])[rel]").map {
+ val doc = Jsoup.parse(it)
+ L.d(doc.getElementById("main-search_input")?.html())
+ val searchQuery = doc.getElementById("main-search-input")?.text() ?: "Null input"
+ L.d("Search query", searchQuery)
+ doc.select("a:not([rel*='keywords(']):not([href=#])[rel]").map {
element ->
//split text into separate items
- L.v("Search element ${element.attr("href")}")
- val texts = element.select("div").map { (it.text()) }.filter { it.isNotBlank() }
+ L.v("Search element", element.attr("href"))
+ val texts = element.select("div").map { it.text() }.filter { !it.isNullOrBlank() }
val pair = Pair(texts, element.attr("href"))
- L.v("Search element potential $pair")
+ L.v("Search element potential", pair.toString())
pair
}.filter { it.first.isNotEmpty() }
}
- .filter { content -> Pair(content.lastOrNull()?.second, content.size) != previousResult }
+ .filter { it.isNotEmpty() }
+ .filter { Pair(it.last().second, it.size) != previousResult }
.subscribe {
content: List<Pair<List<String>, String>> ->
saveResultFrame(content)
@@ -90,7 +95,7 @@ class SearchWebView(context: Context, val contract: SearchContract) : WebView(co
}
override fun reload() {
- super.loadUrl(FbTab.SEARCH.url)
+ super.loadUrl(FbItem.SEARCH.url)
}
/**
@@ -104,7 +109,8 @@ class SearchWebView(context: Context, val contract: SearchContract) : WebView(co
inner class SearchJSI {
@JavascriptInterface
- fun handleHtml(html: String) {
+ fun handleHtml(html: String?) {
+ html ?: return
L.d("Search received response ${contract.isSearchOpened}")
if (!contract.isSearchOpened) pauseLoad = true
searchSubject.onNext(html)
@@ -117,11 +123,14 @@ class SearchWebView(context: Context, val contract: SearchContract) : WebView(co
L.d("Search loaded successfully")
}
1 -> { //something is not found in the search view; this is effectively useless
- L.eThrow("Search subject error; reverting to full overlay")
+ L.e("Search subject error; reverting to full overlay")
Prefs.searchBar = false
searchSubject.onComplete()
contract.searchOverlayDispose()
}
+ 2 -> {
+ L.v("Search emission received")
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/WebStates.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/WebStates.kt
new file mode 100644
index 00000000..ad1fe467
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/WebStates.kt
@@ -0,0 +1,11 @@
+package com.pitchedapps.frost.web
+
+/**
+ * Created by Allan Wang on 2017-08-08.
+ *
+ * Global variables that are define states or constants for web contents
+ */
+const val WEB_LOAD_DELAY = 50L
+var shouldLoadImages = false
+
+val consoleBlacklist = setOf("edge-chat") \ No newline at end of file
diff --git a/app/src/main/res/layout/material_drawer_header.xml b/app/src/main/res/layout/material_drawer_header.xml
new file mode 100644
index 00000000..21cd20a1
--- /dev/null
+++ b/app/src/main/res/layout/material_drawer_header.xml
@@ -0,0 +1,119 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/material_drawer_account_header_height"
+ android:clickable="true"
+ tools:ignore="all">
+
+ <!--
+ Resolves padding
+ Check with <a href="https://github.com/mikepenz/MaterialDrawer/issues/1907">Issue</a>
+ -->
+
+ <ImageView
+ android:id="@+id/material_drawer_account_header_background"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/material_drawer_account_header_height"
+ android:scaleType="centerCrop" />
+
+ <RelativeLayout
+ android:id="@+id/material_drawer_account_header"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/material_drawer_account_header_height">
+
+ <com.mikepenz.materialdrawer.view.BezelImageView
+ android:id="@+id/material_drawer_account_header_current"
+ android:layout_width="@dimen/material_drawer_account_header_selected"
+ android:layout_height="@dimen/material_drawer_account_header_selected"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginBottom="@dimen/material_drawer_account_header_horizontal_bottom"
+ android:layout_marginLeft="@dimen/material_drawer_vertical_padding"
+ android:layout_marginTop="@dimen/material_drawer_account_header_horizontal_top"
+ android:clickable="true"
+ android:elevation="2dp" />
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignTop="@+id/material_drawer_account_header_current"
+ android:gravity="right">
+
+ <com.mikepenz.materialdrawer.view.BezelImageView
+ android:id="@+id/material_drawer_account_header_small_first"
+ android:layout_width="@dimen/material_drawer_account_header_secondary"
+ android:layout_height="@dimen/material_drawer_account_header_secondary"
+ android:layout_marginRight="@dimen/material_drawer_vertical_padding"
+ android:clickable="true"
+ android:elevation="2dp"
+ android:visibility="gone" />
+
+ <com.mikepenz.materialdrawer.view.BezelImageView
+ android:id="@+id/material_drawer_account_header_small_second"
+ android:layout_width="@dimen/material_drawer_account_header_secondary"
+ android:layout_height="@dimen/material_drawer_account_header_secondary"
+ android:layout_marginRight="@dimen/material_drawer_vertical_padding"
+ android:clickable="true"
+ android:elevation="2dp"
+ android:visibility="gone" />
+
+ <com.mikepenz.materialdrawer.view.BezelImageView
+ android:id="@+id/material_drawer_account_header_small_third"
+ android:layout_width="@dimen/material_drawer_account_header_secondary"
+ android:layout_height="@dimen/material_drawer_account_header_secondary"
+ android:layout_marginRight="@dimen/material_drawer_vertical_padding"
+ android:clickable="true"
+ android:elevation="2dp"
+ android:visibility="gone" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/material_drawer_account_header_text_section"
+ android:layout_width="match_parent"
+ android:layout_height="56dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:layout_marginBottom="@dimen/material_drawer_account_header_dropdown_margin_bottom"
+ android:layout_toLeftOf="@+id/material_drawer_account_header_text_switcher"
+ android:gravity="center_vertical|bottom"
+ android:orientation="vertical"
+ android:paddingEnd="56dp"
+ android:paddingLeft="0dp"
+ android:paddingRight="56dp"
+ android:paddingStart="0dp">
+
+ <TextView
+ android:id="@+id/material_drawer_account_header_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/material_drawer_vertical_padding"
+ android:fontFamily="sans-serif-medium"
+ android:lines="1"
+ android:maxLines="1"
+ android:textSize="@dimen/material_drawer_account_header_text" />
+
+ <TextView
+ android:id="@+id/material_drawer_account_header_email"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/material_drawer_vertical_padding"
+ android:fontFamily="sans-serif"
+ android:lines="1"
+ android:maxLines="1"
+ android:textSize="@dimen/material_drawer_account_header_text" />
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/material_drawer_account_header_text_switcher"
+ android:layout_width="@dimen/material_drawer_account_header_dropdown"
+ android:layout_height="@dimen/material_drawer_account_header_dropdown"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_marginBottom="@dimen/material_drawer_account_header_dropdown_margin_bottom"
+ android:layout_marginRight="@dimen/material_drawer_vertical_padding" />
+
+ </RelativeLayout>
+</FrameLayout> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 412d5f34..85826588 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,11 @@
<resources>
<string name="dev_email" translatable="false">pitchedapps@gmail.com</string>
+ <string name="dev_paypal" translatable="false">https://www.paypal.me/Allanw9</string>
+ <string name="paypal" translatable="false">PayPal</string>
<string name="play_store_package_id" translatable="false">com.pitchedapps.frost</string>
+ <!--Tabs-->
+
<string name="feed">Feed</string>
<string name="most_recent">Most Recent</string>
<string name="top_stories">Top Stories</string>
@@ -23,6 +27,8 @@
<string name="notes">Notes</string>
<string name="on_this_day">On This Day</string>
+ <!--Login-->
+
<string name="loading_account">Getting everything ready…</string>
<string name="welcome">Welcome %s</string>
<string name="select_facebook_account">Select Facebook Account</string>
@@ -38,47 +44,23 @@
<string name="feature_request">Feature Request</string>
<string name="subject">Subject</string>
<string name="share">Share</string>
- <string name="share_link">Share Link</string>
- <string name="debug_link">Debug Link</string>
- <string name="debug_link_subject">Frost for Facebook: Link Debug</string>
- <string name="debug_link_content">Write here. Note that your link may contain private information, but I won\'t be able to see it as the post isn\'t public. The url will still help with debugging though.</string>
- <string name="debug_link_desc">If a link isn\'t loading properly, you can email me so I can help debug it. Clicking okay will open an email request</string>
- <string name="open_link">Open Link</string>
- <string name="copy_link">Copy Link</string>
- <string name="copy_text">Copy Text</string>
- <string name="debug_image_link_subject">Frost for Facebook: Image Link Debug</string>
<string name="web_overlay_swipe_hint">Swipe right to go back to the previous window.</string>
<string name="profile_picture">Profile Picture</string>
- <string name="custom_pro">Custom [Pro]</string>
- <string name="uh_oh">Uh Oh</string>
- <string name="reload">Reload</string>
- <string name="play_store_not_pro">It seems like you are a pro user, but we couldn\'t find your purchasing info. If this error persists, please try clearing the Play Store cache and reinstalling the app.</string>
- <string name="play_store_unsupported">It seems like app version can\'t purchase pro. Please reinstall from the play store if this is a persisting issue.</string>
- <string name="play_store_not_found_pro_query">This is a pro feature, but this app doesn\'t seem to be installed from the Play Store. Please reinstall if this is an issue.</string>
- <string name="play_store_billing_error">Something went wrong. Please try again later.</string>
- <string name="play_thank_you">Thank you!</string>
- <string name="play_purchased_pro">Thank you for your support! Enjoy the pro version.</string>
- <string name="play_already_purchased">Already Purchased</string>
- <string name="play_already_purchased_content">Looks like you\'ve already purchased %s. Enjoy!</string>
- <string name="found_pro">Found Frost Pro!</string>
- <string name="found_pro_desc">Looks like you have frost pro! We\'ll reload the app so you can enjoy the awesome features!</string>
- <string name="restoring_purchases">Restoring purchases…</string>
- <string name="purchases_restored">Purchases Restored</string>
- <string name="purchases_restored_with_pro">Frost Pro has been restored. Enjoy the features!</string>
- <string name="purchases_restored_without_pro">It seems like you don\'t have pro. If this is a persistent issue, contact me and attach your purchase receipt.</string>
-
- <string name="login_id_failed">Login failed; id not found</string>
- <string name="iab_still_in_progress">IAB query is still in progress</string>
<string name="new_message">New Message</string>
<string name="no_text">No text</string>
- <string name="image_download_success">Image downloaded</string>
- <string name="image_download_fail">Image failed to download</string>
- <string name="image_share_failed">Failed to share image</string>
- <string name="downloading_video">Downloading Video</string>
- <string name="downloaded_video">Video Downloaded</string>
- <string name="downloading_file">Downloading File</string>
- <string name="downloaded_file">File Downloaded</string>
+ <!--About-->
+
+ <string name="frost_description">Frost is a fully themable,
+ fully functional alternative to the official Facebook app, made from scratch and proudly open sourced.</string>
+
+ <string name="faq_title">Frost FAQ</string>
+
+ <!--HTML Extractor-->
+
+ <string name="html_extraction_error">An error occurred in the html extraction.</string>
+ <string name="html_extraction_cancelled">The request has been cancelled.</string>
+ <string name="html_extraction_timeout">The request has timed out.</string>
</resources>
diff --git a/app/src/main/res/values/strings_about.xml b/app/src/main/res/values/strings_about.xml
deleted file mode 100644
index 68590718..00000000
--- a/app/src/main/res/values/strings_about.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
- <string name="frost_description">Frost is a fully themable,
- fully functional alternative to the official Facebook app, made from scratch and proudly open sourced.</string>
-
- <string name="faq_title">Frost FAQ</string>
-
- <string name="faq_feature">Can you add feature xxx?</string>
- <string name="faq_feature_desc">I\'m always opened to suggestions,
- and if a feature will enhance your experience, I\'d like to hear it.
- However, please consider taking a look at my
- <a href="https://github.com/AllanWang/Frost-for-Facebook/issues">issue tracker</a></string>
-
- <string name="faq_cannot_scroll_horizontally">I can\'t scroll horizontally in the webviews.</string>
- <string name="faq_cannot_scroll_horizontally_desc">This is known since the viewpager takes priority for horizontal scrolling.
- Frost has addressed this by allowing horizontal web scrolls if you tap and hold before scrolling.</string>
-
- <string name="faq_more_frequent_notifications">Can I get more frequent notifications?</string>
- <string name="faq_more_frequent_notifications_desc">I made the decision to prioritize battery life by using a newer job scheduler for Android.
- This means that your framework picks the best time to fetch the notifications, and the lowest window is 15 minutes.
- This is also why I don\'t require the wakelock permission</string>
-</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings_download.xml b/app/src/main/res/values/strings_download.xml
new file mode 100644
index 00000000..383daf56
--- /dev/null
+++ b/app/src/main/res/values/strings_download.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="image_download_success">Image downloaded</string>
+ <string name="image_download_fail">Image failed to download</string>
+ <string name="image_share_failed">Failed to share image</string>
+
+ <string name="downloading_video">Downloading Video</string>
+ <string name="downloaded_video">Video Downloaded</string>
+ <string name="downloading_file">Downloading File</string>
+ <string name="downloaded_file">File Downloaded</string>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings_play_store.xml b/app/src/main/res/values/strings_play_store.xml
new file mode 100644
index 00000000..8d37ee16
--- /dev/null
+++ b/app/src/main/res/values/strings_play_store.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8" standalone="no"?>
+<resources>
+ <string name="found_pro">Found Frost Pro!</string>
+ <string name="found_pro_desc">Looks like you have frost pro! We\'ll reload the app so you can enjoy the awesome features!</string>
+
+ <string name="play_already_purchased">Already Purchased</string>
+ <string name="play_already_purchased_content">Looks like you\'ve already purchased %s. Enjoy!</string>
+ <string name="play_purchased_pro">Thank you for your support! Enjoy the pro version.</string>
+ <string name="play_store_billing_error">Something went wrong. Please try again later.</string>
+ <string name="play_store_not_found_pro_query">This is a pro feature, but this app doesn\'t seem to be installed from the Play Store. Please reinstall if this is an issue.</string>
+ <string name="play_store_not_pro">It seems like you are a pro user, but we couldn\'t find your purchasing info. If this error persists, please try clearing the Play Store cache and reinstalling the app.</string>
+ <string name="play_store_unsupported">It seems like this app version can\'t purchase pro. Please reinstall from the play store if this is a persistent issue.
+ \nIf you would like to donate without any additional features, you may do so through PayPal.</string>
+ <string name="play_thank_you">Thank you!</string>
+
+ <string name="purchases_restored">Purchases Restored</string>
+ <string name="purchases_restored_with_pro">Frost Pro has been restored. Enjoy the features!</string>
+ <string name="purchases_restored_without_pro">It seems like you don\'t have pro. If this is a persistent issue, contact me and attach your purchase receipt.</string>
+
+ <string name="restoring_purchases">Restoring purchases…</string>
+
+ <string name="custom_pro">Custom [Pro]</string>
+ <string name="uh_oh">Uh Oh</string>
+ <string name="reload">Reload</string>
+</resources>
diff --git a/app/src/main/res/values/strings_pref_behaviour.xml b/app/src/main/res/values/strings_pref_behaviour.xml
index 13924a2d..6563ef69 100644
--- a/app/src/main/res/values/strings_pref_behaviour.xml
+++ b/app/src/main/res/values/strings_pref_behaviour.xml
@@ -9,6 +9,10 @@
<string name="overlay_full_screen_swipe_desc">Swipe right from anywhere on the overlaying web to close the browser. If disabled, only swiping from the left edge will move it.</string>
<string name="viewpager_swipe">Viewpager Swipe</string>
<string name="viewpager_swipe_desc">Allow swiping between the pages in the main view to switch tabs. By default, the swiping automatically stops when you long press on an item, such as the like button. Disabling this will prevent page swiping altogether.</string>
+ <string name="search_bar">Search Bar</string>
+ <string name="search_bar_desc">Enable the search bar instead of a search overlay</string>
+ <string name="force_message_bottom">Force Message Bottom</string>
+ <string name="force_message_bottom_desc">When loading a message thread, trigger a scroll to the bottom of the page rather than loading the page as is.</string>
<string name="exit_confirmation">Exit Confirmation</string>
<string name="exit_confirmation_desc">Show confirmation dialog before exiting the app</string>
<string name="analytics">Analytics</string>
diff --git a/app/src/main/res/values/strings_pref_debug.xml b/app/src/main/res/values/strings_pref_debug.xml
new file mode 100644
index 00000000..d65adf29
--- /dev/null
+++ b/app/src/main/res/values/strings_pref_debug.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="debug_toast_enabled">Debugging section is enabled! Go back to settings.</string>
+
+ <string name="debug_disclaimer_info">Though most private content is automatically removed in the report, some sensitive info may still be visible.
+ \nPlease have a look at the debug report before sending it.
+ \n\nClicking one of the options below will prepare an email response with the web page data.
+ </string>
+
+ <string name="debug_incomplete">Incomplete report</string>
+
+ <string name="debug_report_email_title">Frost for Facebook: Debug Report</string>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings_pref_experimental.xml b/app/src/main/res/values/strings_pref_experimental.xml
index 0f545b3e..10c66f99 100644
--- a/app/src/main/res/values/strings_pref_experimental.xml
+++ b/app/src/main/res/values/strings_pref_experimental.xml
@@ -5,8 +5,7 @@
<string name="experimental_disclaimer_info">Experimental features may be unstable and may never make it to production. Use at your own risk, send feedback, and feel free to disable them if they don\'t work well.</string>
<string name="experimental_by_default">Experimental by Default</string>
<string name="experimental_by_default_desc">Feeling risky or just want to help with debugging? Checking this will enable future experimental functions be default.</string>
- <string name="search_bar">Search Bar</string>
- <string name="search_bar_desc">Enable the search bar instead of a search overlay</string>
+
<string name="verbose_logging">Verbose Logging</string>
<string name="verbose_logging_desc">Enable verbose logging to help with crash reports. Logging will only be sent once an error is encountered, so repeat the issue to notify the dev. This will automatically be disabled if the app restarts.</string>
<string name="restart_frost">Restart Frost</string>
diff --git a/app/src/main/res/values/strings_pref_networks.xml b/app/src/main/res/values/strings_pref_networks.xml
new file mode 100644
index 00000000..29eca24a
--- /dev/null
+++ b/app/src/main/res/values/strings_pref_networks.xml
@@ -0,0 +1,4 @@
+<resources>
+ <string name="network_media_on_metered">Load images on metered network.</string>
+ <string name="network_media_on_metered_desc">If a metered network is detected, Frost will automatically stop all images and videos from loading.</string>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings_pref_notifications.xml b/app/src/main/res/values/strings_pref_notifications.xml
index 00c30b57..3cd953f6 100644
--- a/app/src/main/res/values/strings_pref_notifications.xml
+++ b/app/src/main/res/values/strings_pref_notifications.xml
@@ -19,5 +19,4 @@
<string name="notification_vibrate">Notification vibration</string>
<string name="notification_lights">Notification lights</string>
-
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings_preferences.xml b/app/src/main/res/values/strings_preferences.xml
index c2ba0b36..05335345 100644
--- a/app/src/main/res/values/strings_preferences.xml
+++ b/app/src/main/res/values/strings_preferences.xml
@@ -10,6 +10,9 @@
<string name="behaviour">Behaviour</string>
<string name="behaviour_desc">Define how the app interacts in certain settings</string>
+ <string name="network">Network</string>
+ <string name="network_desc">Define options that affect metered networks</string>
+
<string name="experimental">Experimental</string>
<string name="experimental_desc">Enable early access to potentially unstable features</string>
@@ -19,6 +22,8 @@
<string name="about_frost">About Frost for Facebook</string>
<string name="about_frost_desc">Version, Credits, and FAQs</string>
+ <string name="debug_frost">Frost Debugger</string>
+ <string name="debug_frost_desc">Send html data to help with debugging.</string>
<string name="replay_intro">Replay Introduction</string>
</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings_temp.xml b/app/src/main/res/values/strings_temp.xml
deleted file mode 100644
index 526b102d..00000000
--- a/app/src/main/res/values/strings_temp.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
- <string name="intro_title">Frost now has an intro screen</string>
- <string name="intro_desc">Would you like to see it? You can always replay it under settings</string>
-
-</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings_web_context.xml b/app/src/main/res/values/strings_web_context.xml
new file mode 100644
index 00000000..3b93a202
--- /dev/null
+++ b/app/src/main/res/values/strings_web_context.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="share_link">Share Link</string>
+ <string name="debug_link">Debug Link</string>
+ <string name="debug_link_subject">Frost for Facebook: Link Debug</string>
+ <string name="debug_link_content">Write here. Note that your link may contain private information, but I won\'t be able to see it as the post isn\'t public. The url will still help with debugging though.</string>
+ <string name="debug_link_desc">If a link isn\'t loading properly, you can email me so I can help debug it. Clicking okay will open an email request</string>
+ <string name="open_link">Open Link</string>
+ <string name="copy_link">Copy Link</string>
+ <string name="copy_text">Copy Text</string>
+ <string name="debug_image_link_subject">Frost for Facebook: Image Link Debug</string>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml
index e7681107..a1fedd42 100644
--- a/app/src/main/res/xml/frost_changelog.xml
+++ b/app/src/main/res/xml/frost_changelog.xml
@@ -11,19 +11,29 @@
<!--<version title="Beta Updates" />-->
<version title="Beta Updates"/>
- <item text="Fixed notification titles" />
- <item text="Added support for downloading videos" />
- <item text="" />
+ <item text="Create more robust IM notification fetcher with a timeout" />
+ <item text="Add hidden debugging options for certain views" />
+ <item text="Separate IM and general notification groups" />
+ <item text="Add click actions to group notifications. They will launch the message page or the notification page respectively" />
+ <item text="Add behaviour setting to force message threads to scroll to the bottom after loading." />
+ <item text="Add faq for disabling video auto play" />
<item text="" />
<item text="" />
- <version title="v1.5.0"/>
+ <version title="v1.4.2"/>
<item text="Experimental: Add notifications for messages; report to me if this drains your battery" />
<item text="Add FAQ in the about section" />
<item text="Add video uploading" />
<item text="Add open link option in context menu" />
<item text="Add geolocation" />
<item text="Update theme" />
+ <item text="Fix notification titles" />
+ <item text="ALPHA: Add support for downloading videos (hit the download button)" />
+ <item text="Deny intents for login so the page loads properly (thank you @Zenexer)" />
+ <item text="Reduce injection offset and move injectors to an earlier method" />
+ <item text="Add option to disable media loading on metered network" />
+ <item text="Fix menu section" />
+ <item text="Add more background setters to help transparent themes" />
<version title="v1.4.1"/>
<item text="Add intro pages" />
diff --git a/app/src/main/res/xml/frost_faq.xml b/app/src/main/res/xml/frost_faq.xml
index e46d2d50..1830ed33 100644
--- a/app/src/main/res/xml/frost_faq.xml
+++ b/app/src/main/res/xml/frost_faq.xml
@@ -14,4 +14,12 @@
<answer><![CDATA[I made the decision to prioritize battery life by using a newer job scheduler for Android.
This means that your framework picks the best time to fetch the notifications, and the lowest window is 15 minutes.
This is also why I don&apos;t require the wakelock permission]]></answer>
+
+ <question><![CDATA[Can I disable auto play for videos?]]></question>
+ <answer><![CDATA[Facebook already has a toggle. <br/> Go to menu &rarr; account settings &rarr; videos <br/> and change it from there.]]></answer>
+
+ <!--
+ <question><![CDATA[]]></question>
+ <answer><![CDATA[]]></answer>
+ -->
</resources> \ No newline at end of file
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
index c9d27a1c..91e2149c 100644
--- a/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
+++ b/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
@@ -2,6 +2,7 @@ package com.pitchedapps.frost
import com.pitchedapps.frost.injectors.CssHider
import org.junit.Test
+import kotlin.test.assertEquals
/**
* Created by Allan Wang on 2017-06-14.
@@ -9,7 +10,12 @@ import org.junit.Test
class MiscTest {
@Test
- fun asdf() {
+ fun headerFunction() {
print(CssHider.HEADER.injector.function)
}
+
+ @Test
+ fun nullPair() {
+ assertEquals(Pair<String?, Int>(null, 2), Pair<String?, Int>(null, 2))
+ }
} \ No newline at end of file
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/JsoupCleanerTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/JsoupCleanerTest.kt
new file mode 100644
index 00000000..4ec09ea6
--- /dev/null
+++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/JsoupCleanerTest.kt
@@ -0,0 +1,56 @@
+package com.pitchedapps.frost.utils
+
+import org.junit.Test
+import kotlin.test.assertEquals
+
+/**
+ * Created by Allan Wang on 2017-08-10.
+ */
+class JsoupCleanerTest {
+
+ val whitespaceRegex = Regex("\\s+")
+
+ fun String.cleanWhitespace() = replace("\n", "").replace(whitespaceRegex, " ").replace("> <", "><")
+
+ private fun String.assertCleanHtml(expected: String) {
+ assertEquals(expected.cleanWhitespace(), cleanHtml().cleanWhitespace())
+ }
+
+ private fun String.assertCleanJsoup(expected: String) {
+ assertEquals(expected.cleanWhitespace(), cleanJsoup().cleanWhitespace())
+ }
+
+ private fun String.assertCleanText(expected: String) {
+ assertEquals(expected.cleanWhitespace(), cleanText().cleanWhitespace())
+ }
+
+ @Test
+ fun noChange() {
+ "<a><aa> HI </aa></a>".assertCleanJsoup("<a><aa> HI </aa></a>")
+ }
+
+ @Test
+ fun basicText() {
+ """<div class="test">Hello world</div>""".assertCleanHtml("""<div class="test"></div>""")
+ }
+
+ @Test
+ fun multiLineText() {
+ """<div class="test">Hello
+ world</div>""".assertCleanHtml("""<div class="test"></div>""")
+ }
+
+ @Test
+ fun textRemoval() {
+ """<div>Hello<a>World</a></div>""".assertCleanText("<div><a></a></div>")
+ }
+
+ @Test
+ fun kau() {
+ val html = """<div class="col s12 m6"> <div id="kau" class="card medium sticky-action"> <div class="card-image waves-effect waves-block waves-light"> <img class="activator" src="images/kau.jpg"> <span class="card-title activator background-gradient">KAU</span> </div><div class="card-content"><p>An extensive collection of Kotlin Android Utils</p></div><div class="card-action"> <a href="https://github.com/AllanWang/KAU" target="_blank" class="inline-block">Github</a> <a href="https://allanwang.github.io/KAU/" target="_blank" class="inline-block">Page</a> </div><div class="card-reveal"> <span class="card-title grey-text text-darken-4">KAU<i class="material-icons right">close</i></span> <ul class="browser-default"> <li>Huge package of one line extension functions</li><li>Custom UI views</li><li>Adapter items and animators</li><li>SearchView</li><li>Custom delegates</li></ul> </div></div></div>"""
+ val expected = """<div class="col s12 m6"><div id="kau" class="card medium sticky-action"><div class="card-image waves-effect waves-block waves-light"><img class="activator" src="images/kau.jpg"></div><div class="card-action"><a href="-" target="_blank" class="inline-block"></a><a href="-" target="_blank" class="inline-block"></a></div></div></div>"""
+ html.assertCleanHtml(expected)
+ }
+
+}
+
diff --git a/docs/Changelog.md b/docs/Changelog.md
index c36d2b7f..61d5a984 100644
--- a/docs/Changelog.md
+++ b/docs/Changelog.md
@@ -1,16 +1,27 @@
# Changelog
## Beta Updates
-* Fixed notification titles
-* Added support for downloading videos
+* Create more robust IM notification fetcher with a timeout
+* Add hidden debugging options for certain views
+* Separate IM and general notification groups
+* Add click actions to group notifications. They will launch the message page or the notification page respectively
+* Add behaviour setting to force message threads to scroll to the bottom after loading.
+* Add faq for disabling video auto play
-## v1.5.0
+## v1.4.2
* Experimental: Add notifications for messages; report to me if this drains your battery
* Add FAQ in the about section
* Add video uploading
* Add open link option in context menu
* Add geolocation
* Update theme
+* Fix notification titles
+* ALPHA: Add support for downloading videos (hit the download button)
+* Deny intents for login so the page loads properly (thank you @Zenexer)
+* Reduce injection offset and move injectors to an earlier method
+* Add option to disable media loading on metered network
+* Fix menu section
+* Add more background setters to help transparent themes
## v1.4.1
* Add intro pages
diff --git a/gradle.properties b/gradle.properties
index 0a6e4ac0..9c9fe04c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,11 +17,12 @@ MIN_SDK=21
TARGET_SDK=26
BUILD_TOOLS=26.0.1
-KAU=9d3169f
+KAU=4d3d570
KOTLIN=1.1.3-2
+
CRASHLYTICS=2.6.8
DBFLOW=4.0.5
-IAB=1.0.43
+IAB=1.0.44
IICON_COMMUNITY=1.9.32.2
IICON_MATERIAL=2.2.0.3
JSOUP=1.10.3
@@ -30,5 +31,12 @@ MATERIAL_DRAWER_KT=1.0.6
OKHTTP=3.8.1
PAPER_PARCEL=2.0.1
ROBOELECTRIC=3.4
+RX_ANDROID=2.0.1
+RX_BINDING=2.0.0
+RX_JAVA=2.1.2
+RX_KOTLIN=2.1.0
+RX_NETWORK=0.11.0
SCALE_IMAGE_VIEW=3.6.0
-SLIDING_PANEL=3.3.1 \ No newline at end of file
+SLIDING_PANEL=3.3.1
+
+android.enableAapt2=false \ No newline at end of file