aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md23
-rw-r--r--.github/ISSUE_TEMPLATE/feature-request.md12
-rw-r--r--.github/ISSUE_TEMPLATE/theme-report.md16
-rw-r--r--README.md2
-rw-r--r--app/build.gradle17
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt48
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt48
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt85
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt54
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt62
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt145
-rw-r--r--app/src/main/AndroidManifest.xml27
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt11
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt45
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt18
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt128
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt31
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt23
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt40
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt12
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt86
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt90
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt28
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt106
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt (renamed from app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt)24
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt71
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt202
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt92
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt39
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt72
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt12
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt35
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt97
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt16
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt13
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt50
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt20
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt9
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt23
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt8
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt20
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt17
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt194
-rw-r--r--app/src/main/res/drawable/ic_fdroid_24.xml5
-rw-r--r--app/src/main/res/drawable/notification_widget_preview.xml48
-rw-r--r--app/src/main/res/layout/widget_notification_item.xml46
-rw-r--r--app/src/main/res/layout/widget_notifications.xml31
-rw-r--r--app/src/main/res/values/dimens.xml2
-rw-r--r--app/src/main/res/values/strings.xml12
-rw-r--r--app/src/main/res/values/strings_no_translate.xml1
-rw-r--r--app/src/main/res/values/strings_pref_behaviour.xml2
-rw-r--r--app/src/main/res/xml/frost_changelog.xml17
-rw-r--r--app/src/main/res/xml/notification_widget_info.xml10
-rw-r--r--app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json195
-rw-r--r--app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json40
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt2
-rw-r--r--app/src/web/assets/css/core/_core_bg.scss10
-rw-r--r--app/src/web/assets/css/core/_core_border.scss8
-rw-r--r--app/src/web/assets/css/core/_core_text.scss13
-rw-r--r--app/src/web/assets/css/core/core.css31
-rw-r--r--app/src/web/assets/css/themes/custom.css31
-rw-r--r--app/src/web/assets/css/themes/material_amoled.css31
-rw-r--r--app/src/web/assets/css/themes/material_dark.css31
-rw-r--r--app/src/web/assets/css/themes/material_glass.css31
-rw-r--r--app/src/web/assets/css/themes/material_light.css31
-rw-r--r--docs/Changelog.md11
-rw-r--r--gradle.properties4
78 files changed, 2309 insertions, 566 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..1642d528
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,23 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug, unverified
+assignees: AllanWang
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behaviour:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Details (please provide at least the app version):**
+ - App Version:
+ - Device:
+ - Android Version:
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
new file mode 100644
index 00000000..ceb35384
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -0,0 +1,12 @@
+---
+name: Feature Request
+about: Propose a new feature
+title: ''
+labels: feature request
+assignees: AllanWang
+
+---
+
+**Describe the feature**
+<!-- Note that Frost is limited by Facebook's mobile site -->
+<!-- Features relating to the web content are not always possible -->
diff --git a/.github/ISSUE_TEMPLATE/theme-report.md b/.github/ISSUE_TEMPLATE/theme-report.md
new file mode 100644
index 00000000..820fb3f1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/theme-report.md
@@ -0,0 +1,16 @@
+---
+name: Theme report
+about: Problem with Frost's Theme
+title: ''
+labels: CSS
+assignees: AllanWang
+
+---
+
+**Description**
+<!-- Please describe the theme error, and how to get to the page-->
+
+**Details (please provide at least the app version):**
+ - App Version:
+ - Device:
+ - Android Version:
diff --git a/README.md b/README.md
index 30191f8f..5d2330d2 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Frost-for-Facebook
[![Releaes Version](https://img.shields.io/github/release/AllanWang/Frost-for-Facebook.svg)](https://github.com/AllanWang/Frost-for-Facebook/releases)
-[![Build Status](https://travis-ci.org/AllanWang/Frost-for-Facebook.svg?branch=dev)](https://travis-ci.org/AllanWang/Frost-for-Facebook)
+[![Build Status](https://travis-ci.com/AllanWang/Frost-for-Facebook.svg?branch=dev)](https://travis-ci.com/AllanWang/Frost-for-Facebook)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/frost-for-facebook/localized.svg)](https://crowdin.com/project/frost-for-facebook)
[![ZenHub](https://img.shields.io/badge/Shipping%20faster%20with-ZenHub-45529A.svg)](https://app.zenhub.com/workspace/o/allanwang/frost-for-facebook/boards)
[![BugSnag](https://img.shields.io/badge/Bug%20tracking%20with-BugSnag-37C2D9.svg)](https://www.bugsnag.com/)
diff --git a/app/build.gradle b/app/build.gradle
index 409a109b..472669cc 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -27,6 +27,11 @@ android {
versionName androidGitVersion.name()
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments = ["room.schemaLocation": "$projectDir/src/schemas".toString()]
+ }
+ }
}
applicationVariants.all { variant ->
@@ -174,6 +179,7 @@ dependencies {
androidTestImplementation kauDependency.espresso
androidTestImplementation kauDependency.testRules
androidTestImplementation kauDependency.testRunner
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}"
testImplementation kauDependency.kotlinTest
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}"
@@ -200,9 +206,9 @@ dependencies {
implementation "androidx.core:core-ktx:${KTX}"
-// implementation "org.koin:koin-android:${KOIN}"
-// testImplementation "org.koin:koin-test:${KOIN}"
-// androidTestImplementation "org.koin:koin-test:${KOIN}"
+ implementation "org.koin:koin-android:${KOIN}"
+ testImplementation "org.koin:koin-test:${KOIN}"
+ androidTestImplementation "org.koin:koin-test:${KOIN}"
// androidTestImplementation "io.mockk:mockk:${MOCKK}"
@@ -253,6 +259,11 @@ dependencies {
implementation "com.sothree.slidinguppanel:library:${SLIDING_PANEL}"
+ implementation "androidx.room:room-coroutines:${ROOM}"
+ implementation "androidx.room:room-runtime:${ROOM}"
+ kapt "androidx.room:room-compiler:${ROOM}"
+ testImplementation "androidx.room:room-testing:${ROOM}"
+
}
// Validates code and generates apk
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt
new file mode 100644
index 00000000..bae56e2f
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/BaseDbTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.runner.RunWith
+import kotlin.test.AfterTest
+import kotlin.test.BeforeTest
+
+@RunWith(AndroidJUnit4::class)
+abstract class BaseDbTest {
+
+ protected lateinit var db: FrostDatabase
+
+ @BeforeTest
+ fun before() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val privateDb = Room.inMemoryDatabaseBuilder(
+ context, FrostPrivateDatabase::class.java
+ ).build()
+ val publicDb = Room.inMemoryDatabaseBuilder(
+ context, FrostPublicDatabase::class.java
+ ).build()
+ db = FrostDatabase(privateDb, publicDb)
+ }
+
+ @AfterTest
+ fun after() {
+ db.close()
+ }
+}
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt
new file mode 100644
index 00000000..417c6678
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import kotlinx.coroutines.runBlocking
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class CacheDbTest : BaseDbTest() {
+
+ private val dao get() = db.cacheDao()
+ private val cookieDao get() = db.cookieDao()
+
+ private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
+
+ @Test
+ fun save() {
+ val cookie = cookie(1L)
+ val type = "test"
+ val content = "long test".repeat(10000)
+ runBlocking {
+ cookieDao.save(cookie)
+ dao.save(cookie.id, type, content)
+ val cache = dao.select(cookie.id, type) ?: fail("Cache not found")
+ assertEquals(content, cache.contents, "Content mismatch")
+ assertTrue(
+ System.currentTimeMillis() - cache.lastUpdated < 500,
+ "Cache retrieval took over 500ms (${System.currentTimeMillis() - cache.lastUpdated})"
+ )
+ }
+ }
+}
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt
new file mode 100644
index 00000000..327ead86
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import kotlinx.coroutines.runBlocking
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class CookieDbTest : BaseDbTest() {
+
+ private val dao get() = db.cookieDao()
+
+ @Test
+ fun basicCookie() {
+ val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
+ runBlocking {
+ dao.save(cookie)
+ val cookies = dao.selectAll()
+ assertEquals(listOf(cookie), cookies, "Cookie mismatch")
+ }
+ }
+
+ @Test
+ fun deleteCookie() {
+ val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
+
+ runBlocking {
+ dao.save(cookie)
+ dao.deleteById(cookie.id + 1)
+ assertEquals(
+ listOf(cookie),
+ dao.selectAll(),
+ "Cookie list should be the same after inexistent deletion"
+ )
+ dao.deleteById(cookie.id)
+ assertEquals(emptyList(), dao.selectAll(), "Cookie list should be empty after deletion")
+ }
+ }
+
+ @Test
+ fun insertReplaceCookie() {
+ val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
+ runBlocking {
+ dao.save(cookie)
+ assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed")
+ dao.save(cookie.copy(name = "testName2"))
+ assertEquals(
+ listOf(cookie.copy(name = "testName2")),
+ dao.selectAll(),
+ "Cookie replacement failed"
+ )
+ dao.save(cookie.copy(id = 123L))
+ assertEquals(
+ setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")),
+ dao.selectAll().toSet(),
+ "New cookie insertion failed"
+ )
+ }
+ }
+
+ @Test
+ fun selectCookie() {
+ val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie")
+ runBlocking {
+ dao.save(cookie)
+ assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed")
+ assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed")
+ }
+ }
+}
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt
new file mode 100644
index 00000000..c79d212e
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.runner.RunWith
+import org.koin.error.NoBeanDefFoundException
+import org.koin.standalone.get
+import org.koin.test.KoinTest
+import kotlin.reflect.KClass
+import kotlin.reflect.full.functions
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+class DatabaseTest : KoinTest {
+
+ inline fun <reified T : Any> hasKoin() = hasKoin(T::class)
+
+ fun <T : Any> hasKoin(klazz: KClass<T>): Boolean =
+ try {
+ get<T>(clazz = klazz)
+ true
+ } catch (e: NoBeanDefFoundException) {
+ false
+ }
+
+ /**
+ * Database and all daos should be loaded as components
+ */
+ @Test
+ fun testKoins() {
+ hasKoin<FrostDatabase>()
+ val members = FrostDatabase::class.java.kotlin.functions.filter { it.name.endsWith("Dao") }
+ .mapNotNull { it.returnType.classifier as? KClass<*> }
+ assertTrue(members.isNotEmpty(), "Failed to find dao interfaces")
+ val missingKoins = (members + FrostDatabase::class).filter { !hasKoin(it) }
+ assertTrue(missingKoins.isEmpty(), "Missing koins: $missingKoins")
+ }
+}
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt
new file mode 100644
index 00000000..add9f509
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/GenericDbTest.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import com.pitchedapps.frost.facebook.FbItem
+import com.pitchedapps.frost.facebook.defaultTabs
+import kotlinx.coroutines.runBlocking
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class GenericDbTest : BaseDbTest() {
+
+ private val dao get() = db.genericDao()
+
+ /**
+ * Note that order is also preserved here
+ */
+ @Test
+ fun save() {
+ val tabs = listOf(FbItem.ACTIVITY_LOG, FbItem.BIRTHDAYS, FbItem.EVENTS, FbItem.MARKETPLACE, FbItem.ACTIVITY_LOG)
+ runBlocking {
+ dao.saveTabs(tabs)
+ assertEquals(tabs, dao.getTabs(), "Tab saving failed")
+ val newTabs = listOf(FbItem.PAGES, FbItem.MENU)
+ dao.saveTabs(newTabs)
+ assertEquals(newTabs, dao.getTabs(), "Tab overwrite failed")
+ }
+ }
+
+ @Test
+ fun defaultRetrieve() {
+ runBlocking {
+ assertEquals(defaultTabs(), dao.getTabs(), "Default retrieval failed")
+ }
+ }
+
+ @Test
+ fun ignoreErrors() {
+ runBlocking {
+ dao.save(GenericEntity(GenericDao.TYPE_TABS, "${FbItem.ACTIVITY_LOG.name},unknown,${FbItem.EVENTS.name}"))
+ assertEquals(
+ listOf(FbItem.ACTIVITY_LOG, FbItem.EVENTS),
+ dao.getTabs(),
+ "Tab fetching does not ignore unknown names"
+ )
+ }
+ }
+}
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt
new file mode 100644
index 00000000..45a09cbe
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL
+import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES
+import com.pitchedapps.frost.services.NotificationContent
+import kotlinx.coroutines.runBlocking
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class NotificationDbTest : BaseDbTest() {
+
+ private val dao get() = db.notifDao()
+
+ private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id")
+
+ private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) = NotificationContent(
+ data = cookie,
+ id = id,
+ href = "",
+ title = null,
+ text = "",
+ timestamp = time,
+ profileUrl = null,
+ unread = true
+ )
+
+ @Test
+ fun saveAndRetrieve() {
+ val cookie = cookie(12345L)
+ // Unique unsorted ids
+ val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
+ runBlocking {
+ db.cookieDao().save(cookie)
+ dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
+ val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
+ assertEquals(notifs.sortedByDescending { it.timestamp }, dbNotifs, "Incorrect notification list received")
+ }
+ }
+
+ @Test
+ fun selectConditions() {
+ runBlocking {
+ val cookie1 = cookie(12345L)
+ val cookie2 = cookie(12L)
+ val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
+ val notifs2 = (5L..10L).map { notifContent(it, cookie2) }
+ db.cookieDao().save(cookie1)
+ db.cookieDao().save(cookie2)
+ dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1)
+ dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2)
+ assertEquals(
+ emptyList(),
+ dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_MESSAGES),
+ "Filtering by type did not work for cookie1"
+ )
+ assertEquals(
+ notifs1.sortedByDescending { it.timestamp },
+ dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_GENERAL),
+ "Selection for cookie1 failed"
+ )
+ assertEquals(
+ emptyList(),
+ dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_GENERAL),
+ "Filtering by type did not work for cookie2"
+ )
+ assertEquals(
+ notifs2.sortedByDescending { it.timestamp },
+ dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_MESSAGES),
+ "Selection for cookie2 failed"
+ )
+ }
+ }
+
+ /**
+ * Primary key is both id and userId, in the event that the same notification to multiple users has the same id
+ */
+ @Test
+ fun primaryKeyCheck() {
+ runBlocking {
+ val cookie1 = cookie(12345L)
+ val cookie2 = cookie(12L)
+ val notifs1 = (0L..2L).map { notifContent(it, cookie1) }
+ val notifs2 = notifs1.map { it.copy(data = cookie2) }
+ db.cookieDao().save(cookie1)
+ db.cookieDao().save(cookie2)
+ assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed")
+ assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed")
+ }
+ }
+
+ @Test
+ fun cascadeDeletion() {
+ val cookie = cookie(12345L)
+ // Unique unsorted ids
+ val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
+ runBlocking {
+ db.cookieDao().save(cookie)
+ dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
+ db.cookieDao().deleteById(cookie.id)
+ val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL)
+ assertTrue(dbNotifs.isEmpty(), "Cascade deletion failed")
+ }
+ }
+
+ @Test
+ fun latestEpoch() {
+ val cookie = cookie(12345L)
+ // Unique unsorted ids
+ val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) }
+ runBlocking {
+ assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed")
+ db.cookieDao().save(cookie)
+ dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs)
+ assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed")
+ }
+ }
+
+ @Test
+ fun insertionWithInvalidCookies() {
+ runBlocking {
+ assertFalse(
+ dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))),
+ "Notif save should not have passed without relevant cookie entries"
+ )
+ }
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ad1fcbdc..1ca91d62 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,6 +25,17 @@
android:theme="@style/FrostTheme"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute">
+ <!-- Matches general label under NotificationUtils.kt -->
+ <meta-data
+ android:name="com.google.firebase.messaging.default_notification_channel_id"
+ android:value="general" />
+ <meta-data
+ android:name="com.google.firebase.messaging.default_notification_color"
+ android:resource="@color/facebook_blue" />
+ <meta-data
+ android:name="com.google.firebase.messaging.default_notification_icon"
+ android:resource="@drawable/frost_f_24" />
+
<activity
android:name=".StartActivity"
android:label="@string/frost_name"
@@ -48,7 +59,7 @@
android:launchMode="singleTop"
android:theme="@style/FrostTheme.Overlay.Slide" />
<activity
- android:name=".activities.WebOverlayBasicActivity"
+ android:name=".activities.WebOverlayDesktopActivity"
android:configChanges="orientation|screenSize|locale"
android:hardwareAccelerated="true"
android:label="@string/frost_web"
@@ -162,6 +173,20 @@
</intent-filter>
</receiver>
+ <!--Widgets-->
+ <receiver android:name=".widgets.NotificationWidget">
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+ <meta-data
+ android:name="android.appwidget.provider"
+ android:resource="@xml/notification_widget_info" />
+ </receiver>
+ <service
+ android:name=".widgets.NotificationWidgetService"
+ android:enabled="true"
+ android:permission="android.permission.BIND_REMOTEVIEWS" />
+
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
index 5b62afad..41c6ff4b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
@@ -29,9 +29,10 @@ import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ApplicationVersionSignature
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
-import com.pitchedapps.frost.dbflow.CookiesDb
-import com.pitchedapps.frost.dbflow.FbTabsDb
-import com.pitchedapps.frost.dbflow.NotificationDb
+import com.pitchedapps.frost.db.CookiesDb
+import com.pitchedapps.frost.db.FbTabsDb
+import com.pitchedapps.frost.db.FrostDatabase
+import com.pitchedapps.frost.db.NotificationDb
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.services.scheduleNotifications
import com.pitchedapps.frost.services.setupNotificationChannels
@@ -44,6 +45,7 @@ import com.raizlabs.android.dbflow.config.DatabaseConfig
import com.raizlabs.android.dbflow.config.FlowConfig
import com.raizlabs.android.dbflow.config.FlowManager
import com.raizlabs.android.dbflow.runtime.ContentResolverNotifier
+import org.koin.android.ext.android.startKoin
import java.util.Random
import kotlin.reflect.KClass
@@ -81,7 +83,7 @@ class FrostApp : Application() {
)
Showcase.initialize(this, "${BuildConfig.APPLICATION_ID}.showcase")
Prefs.initialize(this, "${BuildConfig.APPLICATION_ID}.prefs")
- // if (LeakCanary.isInAnalyzerProcess(this)) return
+// if (LeakCanary.isInAnalyzerProcess(this)) return
// refWatcher = LeakCanary.install(this)
initBugsnag()
KL.shouldLog = { BuildConfig.DEBUG }
@@ -132,6 +134,7 @@ class FrostApp : Application() {
L.d { "Activity ${activity.localClassName} created" }
}
})
+ startKoin(this, listOf(FrostDatabase.module(this)))
}
private fun initBugsnag() {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
index d0376144..18ae4b0b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt
@@ -32,17 +32,27 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.activities.LoginActivity
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SelectorActivity
-import com.pitchedapps.frost.dbflow.CookieModel
-import com.pitchedapps.frost.dbflow.loadFbCookiesSync
+import com.pitchedapps.frost.db.CookieDao
+import com.pitchedapps.frost.db.CookieEntity
+import com.pitchedapps.frost.db.CookieModel
+import com.pitchedapps.frost.db.FbTabModel
+import com.pitchedapps.frost.db.GenericDao
+import com.pitchedapps.frost.db.getTabs
+import com.pitchedapps.frost.db.save
+import com.pitchedapps.frost.db.saveTabs
+import com.pitchedapps.frost.db.selectAll
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.utils.EXTRA_COOKIES
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.launchNewTask
import com.pitchedapps.frost.utils.loadAssets
+import com.raizlabs.android.dbflow.kotlinextensions.from
+import com.raizlabs.android.dbflow.kotlinextensions.select
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.koin.android.ext.android.inject
import java.util.ArrayList
/**
@@ -50,6 +60,9 @@ import java.util.ArrayList
*/
class StartActivity : KauBaseActivity() {
+ private val cookieDao: CookieDao by inject()
+ private val genericDao: GenericDao by inject()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -67,12 +80,11 @@ class StartActivity : KauBaseActivity() {
launch {
try {
+ migrate()
FbCookie.switchBackUser()
- val cookies = ArrayList(withContext(Dispatchers.IO) {
- loadFbCookiesSync()
- })
+ val cookies = ArrayList(cookieDao.selectAll())
L.i { "Cookies loaded at time ${System.currentTimeMillis()}" }
- L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieModel::toSensitiveString)}" }
+ L._d { "Cookies: ${cookies.joinToString("\t", transform = CookieEntity::toSensitiveString)}" }
loadAssets()
when {
cookies.isEmpty() -> launchNewTask<LoginActivity>()
@@ -85,11 +97,32 @@ class StartActivity : KauBaseActivity() {
})
}
} catch (e: Exception) {
+ L._e(e) { "Load start failed" }
showInvalidWebView()
}
}
}
+ /**
+ * Migrate from dbflow to room
+ * TODO delete dbflow data
+ */
+ private suspend fun migrate() = withContext(Dispatchers.IO) {
+ if (cookieDao.selectAll().isNotEmpty()) return@withContext
+ val cookies = (select from CookieModel::class).queryList().map { CookieEntity(it.id, it.name, it.cookie) }
+ if (cookies.isNotEmpty()) {
+ cookieDao.save(cookies)
+ L._d { "Migrated cookies ${cookieDao.selectAll()}" }
+ }
+ val tabs = (select from FbTabModel::class).queryList().map(FbTabModel::tab)
+ if (tabs.isNotEmpty()) {
+ genericDao.saveTabs(tabs)
+ L._d { "Migrated tabs ${genericDao.getTabs()}" }
+ }
+ deleteDatabase("Cookies.db")
+ deleteDatabase("FrostTabs.db")
+ }
+
private fun showInvalidWebView() =
showInvalidView(R.string.error_webview)
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 f4c1244f..56125303 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
@@ -30,6 +30,7 @@ import ca.allanwang.kau.adapters.ThemableIItemDelegate
import ca.allanwang.kau.logging.KL
import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.dimenPixelSize
+import ca.allanwang.kau.utils.drawable
import ca.allanwang.kau.utils.resolveDrawable
import ca.allanwang.kau.utils.startLink
import ca.allanwang.kau.utils.string
@@ -156,17 +157,26 @@ class AboutActivity : AboutActivityBase(null, {
init {
val c = itemView.context
val size = c.dimenPixelSize(R.dimen.kau_avatar_bounds)
- images = arrayOf<Pair<IIcon, () -> Unit>>(
+
+ val icons: Array<Pair<Int, () -> Unit>> =
+ arrayOf(R.drawable.ic_fdroid_24 to { c.startLink(R.string.fdroid_url) })
+ val iicons: Array<Pair<IIcon, () -> Unit>> = arrayOf(
GoogleMaterial.Icon.gmd_file_download to { c.startLink(R.string.github_downloads_url) },
CommunityMaterial.Icon2.cmd_reddit to { c.startLink(R.string.reddit_url) },
CommunityMaterial.Icon.cmd_github_circle to { c.startLink(R.string.github_url) },
CommunityMaterial.Icon2.cmd_slack to { c.startLink(R.string.slack_url) },
- CommunityMaterial.Icon2.cmd_xda to { c.startLink(R.string.xda_url) }
- ).mapIndexed { i, (icon, onClick) ->
+ CommunityMaterial.Icon2.cmd_xda to { c.startLink(R.string.xda_url) })
+
+ images = (icons.map { (icon, onClick) -> c.drawable(icon) to onClick } + iicons.map { (icon, onClick) ->
+ icon.toDrawable(
+ c,
+ 32
+ ) to onClick
+ }).mapIndexed { i, (icon, onClick) ->
ImageView(c).apply {
layoutParams = ViewGroup.LayoutParams(size, size)
id = 109389 + i
- setImageDrawable(icon.toDrawable(context, 32))
+ setImageDrawable(icon)
scaleType = ImageView.ScaleType.CENTER
background = context.resolveDrawable(android.R.attr.selectableItemBackgroundBorderless)
setOnClickListener { onClick() }
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
index f1d88bc3..d8b29ddc 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt
@@ -69,9 +69,10 @@ import com.pitchedapps.frost.contracts.FileChooserContract
import com.pitchedapps.frost.contracts.FileChooserDelegate
import com.pitchedapps.frost.contracts.MainActivityContract
import com.pitchedapps.frost.contracts.VideoViewHolder
-import com.pitchedapps.frost.dbflow.TAB_COUNT
-import com.pitchedapps.frost.dbflow.loadFbCookie
-import com.pitchedapps.frost.dbflow.loadFbTabs
+import com.pitchedapps.frost.db.CookieDao
+import com.pitchedapps.frost.db.GenericDao
+import com.pitchedapps.frost.db.currentCookie
+import com.pitchedapps.frost.db.getTabs
import com.pitchedapps.frost.enums.MainActivityLayout
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
@@ -103,12 +104,14 @@ import com.pitchedapps.frost.utils.setFrostColors
import com.pitchedapps.frost.views.BadgedIcon
import com.pitchedapps.frost.views.FrostVideoViewer
import com.pitchedapps.frost.views.FrostViewPager
+import com.pitchedapps.frost.widgets.NotificationWidget
import kotlinx.android.synthetic.main.activity_frame_wrapper.*
import kotlinx.android.synthetic.main.view_main_fab.*
import kotlinx.android.synthetic.main.view_main_toolbar.*
import kotlinx.android.synthetic.main.view_main_viewpager.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
+import org.koin.android.ext.android.inject
/**
* Created by Allan Wang on 20/12/17.
@@ -120,9 +123,14 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
FileChooserContract by FileChooserDelegate(),
VideoViewHolder, SearchViewHolder {
- protected lateinit var adapter: SectionsPagerAdapter
+ /**
+ * Note that tabs themselves are initialized through a coroutine during onCreate
+ */
+ protected val adapter: SectionsPagerAdapter = SectionsPagerAdapter()
override val frameWrapper: FrameLayout get() = frame_wrapper
val viewPager: FrostViewPager get() = container
+ val cookieDao: CookieDao by inject()
+ val genericDao: GenericDao by inject()
/*
* Components with the same id in multiple layout files
@@ -131,6 +139,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
val appBar: AppBarLayout by bindView(R.id.appbar)
val coordinator: CoordinatorLayout by bindView(R.id.main_content)
+ protected var lastPosition = -1
+
override var videoViewer: FrostVideoViewer? = null
private lateinit var drawer: Drawer
private lateinit var drawerHeader: AccountHeader
@@ -151,12 +161,13 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
background(viewPager)
}
setSupportActionBar(toolbar)
- adapter = SectionsPagerAdapter(loadFbTabs())
viewPager.adapter = adapter
- viewPager.offscreenPageLimit = TAB_COUNT
tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor())
onNestedCreate(savedInstanceState)
L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" }
+ launch {
+ adapter.setPages(genericDao.getTabs())
+ }
controlWebview = WebView(this)
if (BuildConfig.VERSION_CODE > Prefs.versionCode) {
Prefs.prevVersionCode = Prefs.versionCode
@@ -274,27 +285,28 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
if (current) launchWebOverlay(FbItem.PROFILE.url)
else when (profile.identifier) {
-2L -> {
- val currentCookie = loadFbCookie(Prefs.userId)
- if (currentCookie == null) {
- toast(R.string.account_not_found)
- launch {
+ // TODO no backpressure support
+ this@BaseMainActivity.launch {
+ val currentCookie = cookieDao.currentCookie()
+ if (currentCookie == null) {
+ toast(R.string.account_not_found)
FbCookie.reset()
launchLogin(cookies(), true)
- }
- } else {
- materialDialogThemed {
- title(R.string.kau_logout)
- content(
- String.format(
- string(R.string.kau_logout_confirm_as_x), currentCookie.name
- ?: Prefs.userId.toString()
+ } else {
+ materialDialogThemed {
+ title(R.string.kau_logout)
+ content(
+ String.format(
+ string(R.string.kau_logout_confirm_as_x),
+ currentCookie.name ?: Prefs.userId.toString()
+ )
)
- )
- positiveText(R.string.kau_yes)
- negativeText(R.string.kau_no)
- onPositive { _, _ ->
- launch {
- FbCookie.logout(this@BaseMainActivity)
+ positiveText(R.string.kau_yes)
+ negativeText(R.string.kau_no)
+ onPositive { _, _ ->
+ this@BaseMainActivity.launch {
+ FbCookie.logout(this@BaseMainActivity)
+ }
}
}
}
@@ -303,7 +315,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
-3L -> launchNewTask<LoginActivity>(clearStack = false)
-4L -> launchNewTask<SelectorActivity>(cookies(), false)
else -> {
- launch {
+ this@BaseMainActivity.launch {
FbCookie.switchUser(profile.identifier)
tabsForEachView { _, view -> view.badgeText = null }
refreshAll()
@@ -439,7 +451,11 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
Runtime.getRuntime().exit(0)
return
}
- if (resultCode and REQUEST_RESTART > 0) return restart()
+ if (resultCode and REQUEST_RESTART > 0) {
+ NotificationWidget.forceUpdate(this)
+ restart()
+ return
+ }
/*
* These results can be stacked
*/
@@ -454,16 +470,12 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
- outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(adapter.forcedFallbacks))
+ adapter.saveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
- adapter.forcedFallbacks.clear()
- adapter.forcedFallbacks.addAll(
- savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK)
- ?: emptyList()
- )
+ adapter.restoreInstanceState(savedInstanceState)
}
override fun onResume() {
@@ -496,6 +508,10 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
}
override fun backConsumer(): Boolean {
+ if (drawer.isDrawerOpen) {
+ drawer.closeDrawer()
+ return true
+ }
if (currentFragment.onBackPressed()) return true
if (Prefs.exitConfirmation) {
materialDialogThemed {
@@ -518,9 +534,48 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
runOnUiThread { adapter.reloadFragment(fragment) }
}
- inner class SectionsPagerAdapter(val pages: List<FbItem>) : FragmentPagerAdapter(supportFragmentManager) {
+ inner class SectionsPagerAdapter : FragmentPagerAdapter(supportFragmentManager) {
+
+ private val pages: MutableList<FbItem> = mutableListOf()
- val forcedFallbacks = mutableSetOf<String>()
+ private val forcedFallbacks = mutableSetOf<String>()
+
+ /**
+ * Update page list and prompt reload
+ */
+ fun setPages(pages: List<FbItem>) {
+ this.pages.clear()
+ this.pages.addAll(pages)
+ notifyDataSetChanged()
+ tabs.removeAllTabs()
+ this.pages.forEachIndexed { index, fbItem ->
+ tabs.addTab(
+ tabs.newTab()
+ .setCustomView(BadgedIcon(this@BaseMainActivity).apply { iicon = fbItem.icon }.also {
+ it.setAllAlpha(if (index == 0) SELECTED_TAB_ALPHA else UNSELECTED_TAB_ALPHA)
+ })
+ )
+ }
+ lastPosition = 0
+ viewPager.setCurrentItem(0, false)
+ viewPager.offscreenPageLimit = pages.size
+ viewPager.post {
+ if (!fragmentChannel.isClosedForSend)
+ fragmentChannel.offer(0)
+ } //trigger hook so title is set
+ }
+
+ fun saveInstanceState(outState: Bundle) {
+ outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(forcedFallbacks))
+ }
+
+ fun restoreInstanceState(savedInstanceState: Bundle) {
+ forcedFallbacks.clear()
+ forcedFallbacks.addAll(
+ savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK)
+ ?: emptyList()
+ )
+ }
fun reloadFragment(fragment: BaseFragment) {
if (fragment is WebFragment) return
@@ -559,4 +614,9 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
PointF(0f, toolbar.height.toFloat())
else
PointF(0f, 0f)
+
+ companion object {
+ const val SELECTED_TAB_ALPHA = 255f
+ const val UNSELECTED_TAB_ALPHA = 128f
+ }
}
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 97abf5a2..ed207896 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
@@ -32,9 +32,10 @@ import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.pitchedapps.frost.R
-import com.pitchedapps.frost.dbflow.CookieModel
-import com.pitchedapps.frost.dbflow.loadFbCookiesSuspend
-import com.pitchedapps.frost.dbflow.saveFbCookie
+import com.pitchedapps.frost.db.CookieDao
+import com.pitchedapps.frost.db.CookieEntity
+import com.pitchedapps.frost.db.save
+import com.pitchedapps.frost.db.selectAll
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.profilePictureUrl
@@ -58,6 +59,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
+import org.koin.android.ext.android.inject
import java.net.UnknownHostException
import kotlin.coroutines.resume
@@ -71,6 +73,7 @@ class LoginActivity : BaseActivity() {
private val swipeRefresh: SwipeRefreshLayout by bindView(R.id.swipe_refresh)
private val textview: AppCompatTextView by bindView(R.id.textview)
private val profile: ImageView by bindView(R.id.profile)
+ private val cookieDao: CookieDao by inject()
private lateinit var profileLoader: RequestManager
private val refreshChannel = Channel<Boolean>(10)
@@ -109,13 +112,13 @@ class LoginActivity : BaseActivity() {
refreshChannel.offer(refreshing)
}
- private suspend fun loadInfo(cookie: CookieModel): Unit = withMainContext {
+ private suspend fun loadInfo(cookie: CookieEntity): Unit = withMainContext {
refresh(true)
val imageDeferred = async { loadProfile(cookie.id) }
val nameDeferred = async { loadUsername(cookie) }
- val name: String = nameDeferred.await()
+ val name: String? = nameDeferred.await()
val foundImage: Boolean = imageDeferred.await()
L._d { "Logged in and received data" }
@@ -126,7 +129,7 @@ class LoginActivity : BaseActivity() {
L._i { cookie }
}
- textview.text = String.format(getString(R.string.welcome), name)
+ textview.text = String.format(getString(R.string.welcome), name ?: "")
textview.fadeIn()
frostEvent("Login", "success" to true)
@@ -134,7 +137,7 @@ class LoginActivity : BaseActivity() {
* The user may have logged into an account that is already in the database
* We will let the db handle duplicates and load it now after the new account has been saved
*/
- val cookies = ArrayList(loadFbCookiesSuspend())
+ val cookies = ArrayList(cookieDao.selectAll())
delay(1000)
if (Showcase.intro)
launchNewTask<IntroActivity>(cookies, true)
@@ -171,23 +174,23 @@ class LoginActivity : BaseActivity() {
}
}
- private suspend fun loadUsername(cookie: CookieModel): String = withContext(Dispatchers.IO) {
- val result: String = try {
+ private suspend fun loadUsername(cookie: CookieEntity): String? = withContext(Dispatchers.IO) {
+ val result: String? = try {
withTimeout(5000) {
frostJsoup(cookie.cookie, FbItem.PROFILE.url).title()
}
} catch (e: Exception) {
if (e !is UnknownHostException)
e.logFrostEvent("Fetch username failed")
- ""
+ null
}
- if (cookie.name?.isNotBlank() == false && result != cookie.name) {
- cookie.name = result
- saveFbCookie(cookie)
+ if (result != null) {
+ cookieDao.save(cookie.copy(name = result))
+ return@withContext result
}
- cookie.name ?: ""
+ return@withContext cookie.name
}
override fun backConsumer(): Boolean {
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 a1dba417..34674cb0 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
@@ -35,7 +35,6 @@ class MainActivity : BaseMainActivity() {
override val fragmentChannel = BroadcastChannel<Int>(10)
override val headerBadgeChannel = BroadcastChannel<String>(Channel.CONFLATED)
- var lastPosition = -1
override fun onNestedCreate(savedInstanceState: Bundle?) {
setupTabs()
@@ -54,23 +53,18 @@ class MainActivity : BaseMainActivity() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
- val delta = positionOffset * (255 - 128).toFloat()
+ val delta = positionOffset * (SELECTED_TAB_ALPHA - UNSELECTED_TAB_ALPHA)
tabsForEachView { tabPosition, view ->
view.setAllAlpha(
when (tabPosition) {
- position -> 255.0f - delta
- position + 1 -> 128.0f + delta
- else -> 128f
+ position -> SELECTED_TAB_ALPHA - delta
+ position + 1 -> UNSELECTED_TAB_ALPHA + delta
+ else -> UNSELECTED_TAB_ALPHA
}
)
}
}
})
- viewPager.post {
- if (!fragmentChannel.isClosedForSend)
- fragmentChannel.offer(0)
- lastPosition = 0
- } //trigger hook so title is set
}
private fun setupTabs() {
@@ -86,8 +80,7 @@ class MainActivity : BaseMainActivity() {
(tab.customView as BadgedIcon).badgeText = null
}
})
- headerBadgeChannel.subscribeDuringJob(this, Dispatchers.IO) {
- html ->
+ headerBadgeChannel.subscribeDuringJob(this, Dispatchers.IO) { html ->
try {
val doc = Jsoup.parse(html)
if (doc.select("[data-sigil=count]").isEmpty())
@@ -116,11 +109,5 @@ class MainActivity : BaseMainActivity() {
L.e(e) { "Header badge error" }
}
}
- adapter.pages.forEach {
- tabs.addTab(
- tabs.newTab()
- .setCustomView(BadgedIcon(this).apply { iicon = it.icon })
- )
- }
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt
index 7f632940..6ad7d3f2 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/TabCustomizerActivity.kt
@@ -25,6 +25,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import ca.allanwang.kau.kotlin.lazyContext
+import ca.allanwang.kau.utils.launchMain
import ca.allanwang.kau.utils.scaleXY
import ca.allanwang.kau.utils.setIcon
import ca.allanwang.kau.utils.withAlpha
@@ -33,14 +34,19 @@ import com.mikepenz.fastadapter_extensions.drag.ItemTouchCallback
import com.mikepenz.fastadapter_extensions.drag.SimpleDragCallback
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R
-import com.pitchedapps.frost.dbflow.TAB_COUNT
-import com.pitchedapps.frost.dbflow.loadFbTabs
-import com.pitchedapps.frost.dbflow.save
+import com.pitchedapps.frost.db.GenericDao
+import com.pitchedapps.frost.db.TAB_COUNT
+import com.pitchedapps.frost.db.getTabs
+import com.pitchedapps.frost.db.saveTabs
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.iitems.TabIItem
+import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.setFrostColors
import kotlinx.android.synthetic.main.activity_tab_customizer.*
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.launch
+import org.koin.android.ext.android.inject
import java.util.Collections
/**
@@ -48,6 +54,8 @@ import java.util.Collections
*/
class TabCustomizerActivity : BaseActivity() {
+ private val genericDao: GenericDao by inject()
+
private val adapter = FastItemAdapter<TabIItem>()
private val wobble = lazyContext { AnimationUtils.loadAnimation(it, R.anim.rotate_delta) }
@@ -65,24 +73,30 @@ class TabCustomizerActivity : BaseActivity() {
divider.setBackgroundColor(Prefs.textColor.withAlpha(30))
instructions.setTextColor(Prefs.textColor)
- val tabs = loadFbTabs().toMutableList()
- val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList()
- remaining.removeAll(tabs)
- tabs.addAll(remaining)
+ launch {
+ val tabs = genericDao.getTabs().toMutableList()
+ L.d { "Tabs $tabs" }
+ val remaining = FbItem.values().filter { it.name[0] != '_' }.toMutableList()
+ remaining.removeAll(tabs)
+ tabs.addAll(remaining)
+ adapter.set(tabs.map(::TabIItem))
- adapter.add(tabs.map(::TabIItem))
- bindSwapper(adapter, tab_recycler)
+ bindSwapper(adapter, tab_recycler)
- adapter.withOnClickListener { view, _, _, _ -> view!!.wobble(); true }
+ adapter.withOnClickListener { view, _, _, _ -> view!!.wobble(); true }
+ }
setResult(Activity.RESULT_CANCELED)
fab_save.setIcon(GoogleMaterial.Icon.gmd_check, Prefs.iconColor)
fab_save.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor)
fab_save.setOnClickListener {
- adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item).save()
- setResult(Activity.RESULT_OK)
- finish()
+ launchMain(NonCancellable) {
+ val tabs = adapter.adapterItems.subList(0, TAB_COUNT).map(TabIItem::item)
+ genericDao.saveTabs(tabs)
+ setResult(Activity.RESULT_OK)
+ finish()
+ }
}
fab_cancel.setIcon(GoogleMaterial.Icon.gmd_close, Prefs.iconColor)
fab_cancel.backgroundTintList = ColorStateList.valueOf(Prefs.accentColor)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
index 1ea655d1..accf9d98 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt
@@ -58,7 +58,7 @@ import com.pitchedapps.frost.enums.OverlayContext
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.kotlin.subscribeDuringJob
import com.pitchedapps.frost.services.FrostRunnable
@@ -141,10 +141,10 @@ class FrostWebActivity : WebOverlayActivityBase(false) {
}
/**
- * Variant that forces a basic user agent. This is largely internal,
+ * Variant that forces a desktop user agent. This is largely internal,
* and is only necessary when we are launching from an existing [WebOverlayActivityBase]
*/
-class WebOverlayBasicActivity : WebOverlayActivityBase(true)
+class WebOverlayDesktopActivity : WebOverlayActivityBase(true)
/**
* Internal overlay for the app; this is tied with the main task and is singleTop as opposed to singleInstance
@@ -153,7 +153,7 @@ class WebOverlayActivity : WebOverlayActivityBase(false)
@SuppressLint("Registered")
@UseExperimental(ExperimentalCoroutinesApi::class)
-open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseActivity(),
+open class WebOverlayActivityBase(private val forceDesktopAgent: Boolean) : BaseActivity(),
ActivityContract, FrostContentContainer,
VideoViewHolder, FileChooserContract by FileChooserDelegate() {
@@ -210,8 +210,8 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseAc
}
with(web) {
- if (forceBasicAgent) //todo check; the webview already adds it dynamically
- userAgentString = USER_AGENT_BASIC
+ if (forceDesktopAgent) //todo check; the webview already adds it dynamically
+ userAgentString = USER_AGENT_DESKTOP
Prefs.prevId = Prefs.userId
launch {
if (userId != Prefs.userId)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt
new file mode 100644
index 00000000..f0dacdc7
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import android.os.Parcelable
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.pitchedapps.frost.utils.L
+import kotlinx.android.parcel.Parcelize
+
+/**
+ * Created by Allan Wang on 2017-05-30.
+ */
+
+/**
+ * Generic cache to store serialized content
+ */
+@Entity(
+ tableName = "frost_cache",
+ primaryKeys = ["id", "type"],
+ foreignKeys = [ForeignKey(
+ entity = CookieEntity::class,
+ parentColumns = ["cookie_id"],
+ childColumns = ["id"],
+ onDelete = ForeignKey.CASCADE
+ )]
+)
+@Parcelize
+data class CacheEntity(
+ val id: Long,
+ val type: String,
+ val lastUpdated: Long,
+ val contents: String
+) : Parcelable
+
+@Dao
+interface CacheDao {
+
+ @Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type")
+ fun _select(id: Long, type: String): CacheEntity?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun _insertCache(cache: CacheEntity)
+
+ @Query("DELETE FROM frost_cache WHERE id = :id AND type = :type")
+ fun _delete(id: Long, type: String)
+}
+
+suspend fun CacheDao.select(id: Long, type: String) = dao {
+ _select(id, type)
+}
+
+suspend fun CacheDao.delete(id: Long, type: String) = dao {
+ _delete(id, type)
+}
+
+/**
+ * Returns true if successful, given that there are constraints to the insertion
+ */
+suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = dao {
+ try {
+ _insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents))
+ true
+ } catch (e: Exception) {
+ L.e(e) { "Cache save failed for $type" }
+ false
+ }
+}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt
new file mode 100644
index 00000000..b81ce365
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.pitchedapps.frost.utils.Prefs
+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.structure.BaseModel
+import kotlinx.android.parcel.Parcelize
+
+/**
+ * Created by Allan Wang on 2017-05-30.
+ */
+
+@Entity(tableName = "cookies")
+@Parcelize
+data class CookieEntity(
+ @androidx.room.PrimaryKey
+ @ColumnInfo(name = "cookie_id")
+ val id: Long,
+ val name: String?,
+ val cookie: String?
+) : Parcelable {
+ override fun toString(): String = "CookieEntity(${hashCode()})"
+
+ fun toSensitiveString(): String = "CookieEntity(id=$id, name=$name, cookie=$cookie)"
+}
+
+@Dao
+interface CookieDao {
+
+ @Query("SELECT * FROM cookies")
+ fun _selectAll(): List<CookieEntity>
+
+ @Query("SELECT * FROM cookies WHERE cookie_id = :id")
+ fun _selectById(id: Long): CookieEntity?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun _save(cookie: CookieEntity)
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun _save(cookies: List<CookieEntity>)
+
+ @Query("DELETE FROM cookies WHERE cookie_id = :id")
+ fun _deleteById(id: Long)
+}
+
+suspend fun CookieDao.selectAll() = dao { _selectAll() }
+suspend fun CookieDao.selectById(id: Long) = dao { _selectById(id) }
+suspend fun CookieDao.save(cookie: CookieEntity) = dao { _save(cookie) }
+suspend fun CookieDao.save(cookies: List<CookieEntity>) = dao { _save(cookies) }
+suspend fun CookieDao.deleteById(id: Long) = dao { _deleteById(id) }
+suspend fun CookieDao.currentCookie() = selectById(Prefs.userId)
+
+@Database(version = CookiesDb.VERSION)
+object CookiesDb {
+ const val NAME = "Cookies"
+ const val VERSION = 2
+}
+
+@Parcelize
+@Table(database = CookiesDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
+data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, var cookie: String? = null) :
+ BaseModel(), Parcelable {
+
+ override fun toString(): String = "CookieModel(${hashCode()})"
+}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt
new file mode 100644
index 00000000..c31aa9b7
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/db/DaoUtils.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * Wraps dao calls to work with coroutines
+ * Non transactional queries were supposed to be fixed in https://issuetracker.google.com/issues/69474692,
+ * but it still requires dispatch from a non ui thread.
+ * This avoids that constraint
+ */
+suspend inline fun <T> dao(crossinline block: () -> T) = withContext(Dispatchers.IO) { block() }
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt
new file mode 100644
index 00000000..67372e23
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import com.pitchedapps.frost.BuildConfig
+import org.koin.dsl.module.module
+import org.koin.standalone.StandAloneContext
+
+interface FrostPrivateDao {
+ fun cookieDao(): CookieDao
+ fun notifDao(): NotificationDao
+ fun cacheDao(): CacheDao
+}
+
+@Database(
+ entities = [CookieEntity::class, NotificationEntity::class, CacheEntity::class],
+ version = 1,
+ exportSchema = true
+)
+abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao {
+ companion object {
+ const val DATABASE_NAME = "frost-priv-db"
+ }
+}
+
+interface FrostPublicDao {
+ fun genericDao(): GenericDao
+}
+
+@Database(entities = [GenericEntity::class], version = 1, exportSchema = true)
+abstract class FrostPublicDatabase : RoomDatabase(), FrostPublicDao {
+ companion object {
+ const val DATABASE_NAME = "frost-db"
+ }
+}
+
+interface FrostDao : FrostPrivateDao, FrostPublicDao {
+ fun close()
+}
+
+/**
+ * Composition of all database interfaces
+ */
+class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val publicDb: FrostPublicDatabase) :
+ FrostDao,
+ FrostPrivateDao by privateDb,
+ FrostPublicDao by publicDb {
+
+ override fun close() {
+ privateDb.close()
+ publicDb.close()
+ }
+
+ companion object {
+
+ private fun <T : RoomDatabase> RoomDatabase.Builder<T>.frostBuild() = if (BuildConfig.DEBUG) {
+ fallbackToDestructiveMigration().build()
+ } else {
+ build()
+ }
+
+ fun create(context: Context): FrostDatabase {
+ val privateDb = Room.databaseBuilder(
+ context, FrostPrivateDatabase::class.java,
+ FrostPrivateDatabase.DATABASE_NAME
+ ).frostBuild()
+ val publicDb = Room.databaseBuilder(
+ context, FrostPublicDatabase::class.java,
+ FrostPublicDatabase.DATABASE_NAME
+ ).frostBuild()
+ return FrostDatabase(privateDb, publicDb)
+ }
+
+ fun module(context: Context) = module {
+ single { create(context) }
+ single { get<FrostDatabase>().cookieDao() }
+ single { get<FrostDatabase>().cacheDao() }
+ single { get<FrostDatabase>().notifDao() }
+ single { get<FrostDatabase>().genericDao() }
+ }
+
+ /**
+ * Get from koin
+ * For the most part, you can retrieve directly from other koin components
+ */
+ fun get(): FrostDatabase = StandAloneContext.getKoin().koinContext.get()
+ }
+}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt
index 9d330169..a704ce82 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/FbTabsDb.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt
@@ -14,23 +14,18 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-package com.pitchedapps.frost.dbflow
+package com.pitchedapps.frost.db
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
import com.raizlabs.android.dbflow.annotation.PrimaryKey
import com.raizlabs.android.dbflow.annotation.Table
-import com.raizlabs.android.dbflow.kotlinextensions.database
-import com.raizlabs.android.dbflow.kotlinextensions.fastSave
-import com.raizlabs.android.dbflow.kotlinextensions.from
-import com.raizlabs.android.dbflow.kotlinextensions.select
import com.raizlabs.android.dbflow.structure.BaseModel
/**
* Created by Allan Wang on 2017-05-30.
*/
+
const val TAB_COUNT = 4
@Database(version = FbTabsDb.VERSION)
@@ -41,18 +36,3 @@ object FbTabsDb {
@Table(database = FbTabsDb::class, allFields = true)
data class FbTabModel(@PrimaryKey var position: Int = -1, var tab: FbItem = FbItem.FEED) : BaseModel()
-
-/**
- * Load tabs synchronously
- * Note that tab length should never be a big number anyways
- */
-fun loadFbTabs(): List<FbItem> {
- val tabs: List<FbTabModel>? = (select from (FbTabModel::class)).orderBy(FbTabModel_Table.position, true).queryList()
- if (tabs?.size == TAB_COUNT) return tabs.map(FbTabModel::tab)
- L.d { "No tabs (${tabs?.size}); loading default" }
- return defaultTabs()
-}
-
-fun List<FbItem>.save() {
- database<FbTabsDb>().beginTransactionAsync(mapIndexed(::FbTabModel).fastSave().build()).execute()
-}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt
new file mode 100644
index 00000000..f36c8af9
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/db/GenericDb.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import com.pitchedapps.frost.facebook.FbItem
+import com.pitchedapps.frost.facebook.defaultTabs
+
+/**
+ * Created by Allan Wang on 2017-05-30.
+ */
+
+/**
+ * Generic cache to store serialized content
+ */
+@Entity(tableName = "frost_generic")
+data class GenericEntity(
+ @PrimaryKey
+ val type: String,
+ val contents: String
+)
+
+@Dao
+interface GenericDao {
+
+ @Query("SELECT contents FROM frost_generic WHERE type = :type")
+ fun _select(type: String): String?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun _save(entity: GenericEntity)
+
+ @Query("DELETE FROM frost_generic WHERE type = :type")
+ fun _delete(type: String)
+
+ companion object {
+ const val TYPE_TABS = "generic_tabs"
+ }
+}
+
+suspend fun GenericDao.saveTabs(tabs: List<FbItem>) = dao {
+ val content = tabs.joinToString(",") { it.name }
+ _save(GenericEntity(GenericDao.TYPE_TABS, content))
+}
+
+suspend fun GenericDao.getTabs(): List<FbItem> = dao {
+ val allTabs = FbItem.values.map { it.name to it }.toMap()
+ _select(GenericDao.TYPE_TABS)
+ ?.split(",")
+ ?.mapNotNull { allTabs[it] }
+ ?.takeIf { it.isNotEmpty() }
+ ?: defaultTabs()
+}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt
new file mode 100644
index 00000000..d4b51c1e
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.db
+
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Embedded
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Transaction
+import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL
+import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES
+import com.pitchedapps.frost.services.NotificationContent
+import com.pitchedapps.frost.utils.L
+import com.raizlabs.android.dbflow.annotation.ConflictAction
+import com.raizlabs.android.dbflow.annotation.Database
+import com.raizlabs.android.dbflow.annotation.Migration
+import com.raizlabs.android.dbflow.annotation.PrimaryKey
+import com.raizlabs.android.dbflow.annotation.Table
+import com.raizlabs.android.dbflow.kotlinextensions.eq
+import com.raizlabs.android.dbflow.kotlinextensions.from
+import com.raizlabs.android.dbflow.kotlinextensions.select
+import com.raizlabs.android.dbflow.kotlinextensions.where
+import com.raizlabs.android.dbflow.sql.SQLiteType
+import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration
+import com.raizlabs.android.dbflow.structure.BaseModel
+
+@Entity(
+ tableName = "notifications",
+ primaryKeys = ["notif_id", "userId"],
+ foreignKeys = [ForeignKey(
+ entity = CookieEntity::class,
+ parentColumns = ["cookie_id"],
+ childColumns = ["userId"],
+ onDelete = ForeignKey.CASCADE
+ )],
+ indices = [Index("notif_id"), Index("userId")]
+)
+data class NotificationEntity(
+ @ColumnInfo(name = "notif_id")
+ val id: Long,
+ val userId: Long,
+ val href: String,
+ val title: String?,
+ val text: String,
+ val timestamp: Long,
+ val profileUrl: String?,
+ // Type essentially refers to channel
+ val type: String,
+ val unread: Boolean
+) {
+ constructor(
+ type: String,
+ content: NotificationContent
+ ) : this(
+ content.id,
+ content.data.id,
+ content.href,
+ content.title,
+ content.text,
+ content.timestamp,
+ content.profileUrl,
+ type,
+ content.unread
+ )
+}
+
+data class NotificationContentEntity(
+ @Embedded
+ val cookie: CookieEntity,
+ @Embedded
+ val notif: NotificationEntity
+) {
+ fun toNotifContent() = NotificationContent(
+ data = cookie,
+ id = notif.id,
+ href = notif.href,
+ title = notif.title,
+ text = notif.text,
+ timestamp = notif.timestamp,
+ profileUrl = notif.profileUrl,
+ unread = notif.unread
+ )
+}
+
+@Dao
+interface NotificationDao {
+
+ /**
+ * Note that notifications are guaranteed to be ordered by descending timestamp
+ */
+ @Transaction
+ @Query("SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC")
+ fun _selectNotifications(userId: Long, type: String): List<NotificationContentEntity>
+
+ @Query("SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1")
+ fun _selectEpoch(userId: Long, type: String): Long?
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ fun _insertNotifications(notifs: List<NotificationEntity>)
+
+ @Query("DELETE FROM notifications WHERE userId = :userId AND type = :type")
+ fun _deleteNotifications(userId: Long, type: String)
+
+ @Query("DELETE FROM notifications")
+ fun _deleteAll()
+
+ /**
+ * It is assumed that the notification batch comes from the same user
+ */
+ @Transaction
+ fun _saveNotifications(type: String, notifs: List<NotificationContent>) {
+ val userId = notifs.firstOrNull()?.data?.id ?: return
+ val entities = notifs.map { NotificationEntity(type, it) }
+ _deleteNotifications(userId, type)
+ _insertNotifications(entities)
+ }
+}
+
+suspend fun NotificationDao.deleteAll() = dao { _deleteAll() }
+
+fun NotificationDao.selectNotificationsSync(userId: Long, type: String): List<NotificationContent> =
+ _selectNotifications(userId, type).map { it.toNotifContent() }
+
+suspend fun NotificationDao.selectNotifications(userId: Long, type: String): List<NotificationContent> = dao {
+ selectNotificationsSync(userId, type)
+}
+
+/**
+ * Returns true if successful, given that there are constraints to the insertion
+ */
+suspend fun NotificationDao.saveNotifications(type: String, notifs: List<NotificationContent>): Boolean {
+ if (notifs.isEmpty()) return true
+ return dao {
+ try {
+ _saveNotifications(type, notifs)
+ true
+ } catch (e: Exception) {
+ L.e(e) { "Notif save failed for $type" }
+ false
+ }
+ }
+}
+
+suspend fun NotificationDao.latestEpoch(userId: Long, type: String): Long = dao {
+ _selectEpoch(userId, type) ?: lastNotificationTime(userId).let {
+ when (type) {
+ NOTIF_CHANNEL_GENERAL -> it.epoch
+ NOTIF_CHANNEL_MESSAGES -> it.epochIm
+ else -> -1L
+ }
+ }
+}
+
+/**
+ * Created by Allan Wang on 2017-05-30.
+ */
+
+@Database(version = NotificationDb.VERSION)
+object NotificationDb {
+ const val NAME = "Notifications"
+ const val VERSION = 2
+}
+
+@Migration(version = 2, database = NotificationDb::class)
+class NotificationMigration2(modelClass: Class<NotificationModel>) :
+ AlterTableMigration<NotificationModel>(modelClass) {
+ override fun onPreMigrate() {
+ super.onPreMigrate()
+ addColumn(SQLiteType.INTEGER, "epochIm")
+ L.d { "Added column" }
+ }
+}
+
+@Table(database = NotificationDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
+data class NotificationModel(
+ @PrimaryKey var id: Long = -1L,
+ var epoch: Long = -1L,
+ var epochIm: Long = -1L
+) : BaseModel()
+
+internal fun lastNotificationTime(id: Long): NotificationModel =
+ (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle()
+ ?: NotificationModel(id = id)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
deleted file mode 100644
index 8411b8d7..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/CookiesDb.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2018 Allan Wang
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package com.pitchedapps.frost.dbflow
-
-import android.os.Parcelable
-import com.pitchedapps.frost.utils.L
-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.async
-import com.raizlabs.android.dbflow.kotlinextensions.delete
-import com.raizlabs.android.dbflow.kotlinextensions.eq
-import com.raizlabs.android.dbflow.kotlinextensions.from
-import com.raizlabs.android.dbflow.kotlinextensions.save
-import com.raizlabs.android.dbflow.kotlinextensions.select
-import com.raizlabs.android.dbflow.kotlinextensions.where
-import com.raizlabs.android.dbflow.structure.BaseModel
-import kotlinx.android.parcel.Parcelize
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-/**
- * Created by Allan Wang on 2017-05-30.
- */
-
-@Database(version = CookiesDb.VERSION)
-object CookiesDb {
- const val NAME = "Cookies"
- const val VERSION = 2
-}
-
-@Parcelize
-@Table(database = CookiesDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
-data class CookieModel(@PrimaryKey var id: Long = -1L, var name: String? = null, var cookie: String? = null) :
- BaseModel(), Parcelable {
-
- override fun toString(): String = "CookieModel(${hashCode()})"
-
- fun toSensitiveString(): String = "CookieModel(id=$id, name=$name, cookie=$cookie)"
-}
-
-fun loadFbCookie(id: Long): CookieModel? =
- (select from CookieModel::class where (CookieModel_Table.id eq id)).querySingle()
-
-fun loadFbCookie(name: String): CookieModel? =
- (select from CookieModel::class where (CookieModel_Table.name eq name)).querySingle()
-
-/**
- * Loads cookies sorted by name
- */
-fun loadFbCookiesAsync(callback: (cookies: List<CookieModel>) -> Unit) {
- (select from CookieModel::class).orderBy(CookieModel_Table.name, true).async()
- .queryListResultCallback { _, tResult -> callback(tResult) }.execute()
-}
-
-fun loadFbCookiesSync(): List<CookieModel> =
- (select from CookieModel::class).orderBy(CookieModel_Table.name, true).queryList()
-
-// TODO temp method until dbflow supports coroutines
-suspend fun loadFbCookiesSuspend(): List<CookieModel> = withContext(Dispatchers.IO) {
- loadFbCookiesSync()
-}
-
-inline fun saveFbCookie(cookie: CookieModel, crossinline callback: (() -> Unit) = {}) {
- cookie.async save {
- L.d { "Fb cookie saved" }
- L._d { cookie.toSensitiveString() }
- callback()
- }
-}
-
-fun removeCookie(id: Long) {
- loadFbCookie(id)?.async?.delete {
- L.d { "Fb cookie deleted" }
- L._d { id }
- }
-}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt
deleted file mode 100644
index 740fef62..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/DbUtils.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright 2018 Allan Wang
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package com.pitchedapps.frost.dbflow
-
-import android.content.Context
-import com.pitchedapps.frost.utils.L
-import com.raizlabs.android.dbflow.config.FlowManager
-import com.raizlabs.android.dbflow.structure.database.transaction.FastStoreModelTransaction
-
-/**
- * Created by Allan Wang on 2017-05-30.
- */
-
-object DbUtils {
-
- fun db(name: String) = FlowManager.getDatabase(name)
- fun dbName(name: String) = "$name.db"
- fun deleteDatabase(c: Context, name: String) = c.deleteDatabase(dbName(name))
-}
-
-inline fun <reified T : Any> List<T>.replace(dbName: String) {
- L.d { "Replacing $dbName.db" }
- DbUtils.db(dbName).reset()
- FastStoreModelTransaction.saveBuilder(FlowManager.getModelAdapter(T::class.java)).addAll(this).build()
-}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt
deleted file mode 100644
index a054d95e..00000000
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2018 Allan Wang
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-package com.pitchedapps.frost.dbflow
-
-import com.pitchedapps.frost.utils.L
-import com.raizlabs.android.dbflow.annotation.ConflictAction
-import com.raizlabs.android.dbflow.annotation.Database
-import com.raizlabs.android.dbflow.annotation.Migration
-import com.raizlabs.android.dbflow.annotation.PrimaryKey
-import com.raizlabs.android.dbflow.annotation.Table
-import com.raizlabs.android.dbflow.kotlinextensions.async
-import com.raizlabs.android.dbflow.kotlinextensions.eq
-import com.raizlabs.android.dbflow.kotlinextensions.from
-import com.raizlabs.android.dbflow.kotlinextensions.save
-import com.raizlabs.android.dbflow.kotlinextensions.select
-import com.raizlabs.android.dbflow.kotlinextensions.where
-import com.raizlabs.android.dbflow.sql.SQLiteType
-import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration
-import com.raizlabs.android.dbflow.structure.BaseModel
-
-/**
- * Created by Allan Wang on 2017-05-30.
- */
-
-@Database(version = NotificationDb.VERSION)
-object NotificationDb {
- const val NAME = "Notifications"
- const val VERSION = 2
-}
-
-@Migration(version = 2, database = NotificationDb::class)
-class NotificationMigration2(modelClass: Class<NotificationModel>) :
- AlterTableMigration<NotificationModel>(modelClass) {
- override fun onPreMigrate() {
- super.onPreMigrate()
- addColumn(SQLiteType.INTEGER, "epochIm")
- L.d { "Added column" }
- }
-}
-
-@Table(database = NotificationDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
-data class NotificationModel(
- @PrimaryKey var id: Long = -1L,
- var epoch: Long = -1L,
- var epochIm: Long = -1L
-) : BaseModel()
-
-fun lastNotificationTime(id: Long): NotificationModel =
- (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle()
- ?: NotificationModel(id = id)
-
-fun saveNotificationTime(notificationModel: NotificationModel, callback: (() -> Unit)? = null) {
- notificationModel.async save {
- L.d { "Fb notification model saved" }
- L._d { notificationModel }
- callback?.invoke()
- }
-}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
index 30c812db..f128edcf 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
@@ -19,7 +19,7 @@ package com.pitchedapps.frost.debugger
import ca.allanwang.kau.logging.KauLoggerExtension
import ca.allanwang.kau.utils.copyFromInputStream
import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.facebook.requests.call
import com.pitchedapps.frost.utils.createFreshDir
@@ -59,7 +59,7 @@ class OfflineWebsite(
* Directory that holds all the files
*/
val baseDir: File,
- private val userAgent: String = USER_AGENT_BASIC
+ private val userAgent: String = USER_AGENT_DESKTOP
) {
/**
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt
index 1d6937f7..f6316470 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt
@@ -22,18 +22,18 @@ package com.pitchedapps.frost.facebook
const val FACEBOOK_COM = "facebook.com"
const val FBCDN_NET = "fbcdn.net"
-const val HTTPS_FACEBOOK_COM = "https://$FACEBOOK_COM"
+const val HTTPS_FACEBOOK_COM = "https://$FACEBOOK_COM/"
const val FB_URL_BASE = "https://m.$FACEBOOK_COM/"
fun profilePictureUrl(id: Long) = "https://graph.facebook.com/$id/picture?type=large"
const val FB_LOGIN_URL = "${FB_URL_BASE}login"
const val FB_HOME_URL = "${FB_URL_BASE}home.php"
// Default user agent
-const val USER_AGENT_FULL =
- "Mozilla/5.0 (Linux; Android 7.1; Mi A1 Build/N2G47H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36"
-// Basic user agent; non mobile version
-const val USER_AGENT_BASIC =
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"
+const val USER_AGENT_MOBILE =
+ "Mozilla/5.0 (Linux; Android 8.0.0; ONEPLUS A3000) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Mobile Safari/537.36"
+// Desktop agent, for pages like messages
+const val USER_AGENT_DESKTOP =
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Safari/537.36"
/**
* Animation transition delay, just to ensure that the styles
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
index 5683526a..0c1da3a3 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
@@ -19,10 +19,12 @@ package com.pitchedapps.frost.facebook
import android.app.Activity
import android.content.Context
import android.webkit.CookieManager
-import com.pitchedapps.frost.dbflow.CookieModel
-import com.pitchedapps.frost.dbflow.loadFbCookie
-import com.pitchedapps.frost.dbflow.removeCookie
-import com.pitchedapps.frost.dbflow.saveFbCookie
+import com.pitchedapps.frost.db.CookieDao
+import com.pitchedapps.frost.db.CookieEntity
+import com.pitchedapps.frost.db.FrostDatabase
+import com.pitchedapps.frost.db.deleteById
+import com.pitchedapps.frost.db.save
+import com.pitchedapps.frost.db.selectById
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.cookies
@@ -50,6 +52,10 @@ object FbCookie {
inline val webCookie: String?
get() = CookieManager.getInstance().getCookie(COOKIE_DOMAIN)
+ private val cookieDao: CookieDao by lazy {
+ FrostDatabase.get().cookieDao()
+ }
+
private suspend fun CookieManager.suspendSetWebCookie(cookie: String?): Boolean {
cookie ?: return true
return withContext(NonCancellable) {
@@ -77,12 +83,12 @@ object FbCookie {
}
}
- fun save(id: Long) {
+ suspend fun save(id: Long) {
L.d { "New cookie found" }
Prefs.userId = id
CookieManager.getInstance().flush()
- val cookie = CookieModel(Prefs.userId, "", webCookie)
- saveFbCookie(cookie)
+ val cookie = CookieEntity(Prefs.userId, null, webCookie)
+ cookieDao.save(cookie)
}
suspend fun reset() {
@@ -93,11 +99,12 @@ object FbCookie {
}
}
- suspend fun switchUser(id: Long) = switchUser(loadFbCookie(id))
-
- suspend fun switchUser(name: String) = switchUser(loadFbCookie(name))
+ suspend fun switchUser(id: Long) {
+ val cookie = cookieDao.selectById(id) ?: return L.e { "No cookie for id" }
+ switchUser(cookie)
+ }
- suspend fun switchUser(cookie: CookieModel?) {
+ suspend fun switchUser(cookie: CookieEntity?) {
if (cookie == null) {
L.d { "Switching User; null cookie" }
return
@@ -114,7 +121,7 @@ object FbCookie {
* and launch the proper login page
*/
suspend fun logout(context: Context) {
- val cookies = arrayListOf<CookieModel>()
+ val cookies = arrayListOf<CookieEntity>()
if (context is Activity)
cookies.addAll(context.cookies().filter { it.id != Prefs.userId })
logout(Prefs.userId)
@@ -126,7 +133,9 @@ object FbCookie {
*/
suspend fun logout(id: Long) {
L.d { "Logging out user" }
- removeCookie(id)
+ cookieDao.deleteById(id)
+ L.d { "Fb cookie deleted" }
+ L._d { id }
reset()
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt
index 90c8848c..24c39e28 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/FrostParser.kt
@@ -16,7 +16,7 @@
*/
package com.pitchedapps.frost.facebook.parsers
-import com.pitchedapps.frost.dbflow.CookieModel
+import com.pitchedapps.frost.db.CookieEntity
import com.pitchedapps.frost.facebook.FB_CSS_URL_MATCHER
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.facebook.get
@@ -85,7 +85,7 @@ interface ParseData {
}
interface ParseNotification : ParseData {
- fun getUnreadNotifications(data: CookieModel): List<NotificationContent>
+ fun getUnreadNotifications(data: CookieEntity): List<NotificationContent>
}
internal fun <T> List<T>.toJsonString(tag: String, indent: Int) = StringBuilder().apply {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt
index 529ac23a..00a1432f 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt
@@ -16,7 +16,7 @@
*/
package com.pitchedapps.frost.facebook.parsers
-import com.pitchedapps.frost.dbflow.CookieModel
+import com.pitchedapps.frost.db.CookieEntity
import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER
import com.pitchedapps.frost.facebook.FB_MESSAGE_NOTIF_ID_MATCHER
import com.pitchedapps.frost.facebook.FbItem
@@ -58,7 +58,7 @@ data class FrostMessages(
append("}")
}.toString()
- override fun getUnreadNotifications(data: CookieModel) =
+ override fun getUnreadNotifications(data: CookieEntity) =
threads.asSequence().filter(FrostThread::unread).map {
with(it) {
NotificationContent(
@@ -68,7 +68,8 @@ data class FrostMessages(
title = title,
text = content ?: "",
timestamp = time,
- profileUrl = img
+ profileUrl = img,
+ unread = unread
)
}
}.toList()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt
index 422ec384..faeaa27c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt
@@ -16,7 +16,7 @@
*/
package com.pitchedapps.frost.facebook.parsers
-import com.pitchedapps.frost.dbflow.CookieModel
+import com.pitchedapps.frost.db.CookieEntity
import com.pitchedapps.frost.facebook.FB_EPOCH_MATCHER
import com.pitchedapps.frost.facebook.FB_NOTIF_ID_MATCHER
import com.pitchedapps.frost.facebook.FbItem
@@ -47,7 +47,7 @@ data class FrostNotifs(
append("}")
}.toString()
- override fun getUnreadNotifications(data: CookieModel) =
+ override fun getUnreadNotifications(data: CookieEntity) =
notifs.asSequence().filter(FrostNotif::unread).map {
with(it) {
NotificationContent(
@@ -57,7 +57,8 @@ data class FrostNotifs(
title = null,
text = content,
timestamp = time,
- profileUrl = img
+ profileUrl = img,
+ unread = unread
)
}
}.toList()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
index b49fd970..8a89b973 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt
@@ -22,7 +22,7 @@ import com.pitchedapps.frost.facebook.FB_JSON_URL_MATCHER
import com.pitchedapps.frost.facebook.FB_REV_MATCHER
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.kotlin.Flyweight
import com.pitchedapps.frost.utils.L
@@ -97,7 +97,7 @@ internal fun List<Pair<String, Any?>>.withEmptyData(vararg key: String): List<Pa
internal fun String?.requestBuilder(): Request.Builder {
val builder = Request.Builder()
- .header("User-Agent", USER_AGENT_BASIC)
+ .header("User-Agent", USER_AGENT_DESKTOP)
if (this != null)
builder.header("Cookie", this)
// .cacheControl(CacheControl.FORCE_NETWORK)
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 54a9f8ae..1c37bc29 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
@@ -31,9 +31,10 @@ 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.NotificationModel
-import com.pitchedapps.frost.dbflow.lastNotificationTime
+import com.pitchedapps.frost.db.CookieEntity
+import com.pitchedapps.frost.db.FrostDatabase
+import com.pitchedapps.frost.db.latestEpoch
+import com.pitchedapps.frost.db.saveNotifications
import com.pitchedapps.frost.enums.OverlayContext
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.parsers.FrostParser
@@ -60,12 +61,10 @@ private val _40_DP = 40.dpToPx
* Enum to handle notification creations
*/
enum class NotificationType(
- private val channelId: String,
+ val channelId: String,
private val overlayContext: OverlayContext,
private val fbItem: FbItem,
private val parser: FrostParser<ParseNotification>,
- private val getTime: (notif: NotificationModel) -> Long,
- private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel,
private val ringtone: () -> String
) {
@@ -74,8 +73,6 @@ enum class NotificationType(
OverlayContext.NOTIFICATION,
FbItem.NOTIFICATIONS,
NotifParser,
- NotificationModel::epoch,
- { notif, time -> notif.copy(epoch = time) },
Prefs::notificationRingtone
) {
@@ -88,8 +85,6 @@ enum class NotificationType(
OverlayContext.MESSAGE,
FbItem.MESSAGES,
MessageParser,
- NotificationModel::epochIm,
- { notif, time -> notif.copy(epochIm = time) },
Prefs::messageRingtone
);
@@ -100,8 +95,8 @@ enum class NotificationType(
*/
internal open fun bindRequest(content: NotificationContent, cookie: String): (BaseBundle.() -> Unit)? = null
- private fun bindRequest(intent: Intent, content: NotificationContent, cookie: String?) {
- cookie ?: return
+ private fun bindRequest(intent: Intent, content: NotificationContent) {
+ val cookie = content.data.cookie ?: return
val binder = bindRequest(content, cookie) ?: return
val bundle = Bundle()
bundle.binder()
@@ -116,7 +111,8 @@ enum class NotificationType(
* Returns the number of notifications generated,
* or -1 if an error occurred
*/
- fun fetch(context: Context, data: CookieModel): Int {
+ suspend fun fetch(context: Context, data: CookieEntity): Int {
+ val notifDao = FrostDatabase.get().notifDao()
val response = try {
parser.parse(data.cookie)
} catch (ignored: Exception) {
@@ -142,36 +138,42 @@ enum class NotificationType(
}
if (notifContents.isEmpty()) return 0
val userId = data.id
- val prevNotifTime = lastNotificationTime(userId)
- val prevLatestEpoch = getTime(prevNotifTime)
+ // Legacy, remove with dbflow
+ val prevLatestEpoch = notifDao.latestEpoch(userId, channelId)
L.v { "Notif $name prev epoch $prevLatestEpoch" }
- var newLatestEpoch = prevLatestEpoch
- val notifs = mutableListOf<FrostNotification>()
- notifContents.forEach { notif ->
- L.v { "Notif timestamp ${notif.timestamp}" }
- if (notif.timestamp <= prevLatestEpoch) return@forEach
- notifs.add(createNotification(context, notif))
- if (notif.timestamp > newLatestEpoch)
- newLatestEpoch = notif.timestamp
- }
- if (newLatestEpoch > prevLatestEpoch)
- putTime(prevNotifTime, newLatestEpoch).save()
- L.d { "Notif $name new epoch ${getTime(lastNotificationTime(userId))}" }
if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) {
L.d { "Skipping first notification fetch" }
return 0 // do not notify the first time
}
+
+ val newNotifContents = notifContents.filter { it.timestamp > prevLatestEpoch }
+
+ if (newNotifContents.isEmpty()) {
+ L.d { "No new notifs found for $name" }
+ return 0
+ }
+
+ L.d { "${newNotifContents.size} new notifs found for $name" }
+
+ if (!notifDao.saveNotifications(channelId, newNotifContents)) {
+ L.d { "Skip notifs for $name as saving failed" }
+ return 0
+ }
+
+ val notifs = newNotifContents.map { createNotification(context, it) }
+
frostEvent("Notifications", "Type" to name, "Count" to notifs.size)
if (notifs.size > 1)
summaryNotification(context, userId, notifs.size).notify(context)
val ringtone = ringtone()
notifs.forEachIndexed { i, notif ->
+ // Ring at most twice
notif.withAlert(i < 2, ringtone).notify(context)
}
return notifs.size
}
- fun debugNotification(context: Context, data: CookieModel) {
+ fun debugNotification(context: Context, data: CookieEntity) {
val content = NotificationContent(
data,
System.currentTimeMillis(),
@@ -179,23 +181,40 @@ enum class NotificationType(
"Debug Notif",
"Test 123",
System.currentTimeMillis() / 1000,
- "https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png"
+ "https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png",
+ false
)
createNotification(context, content).notify(context)
}
/**
+ * Attach content related data to an intent
+ */
+ fun putContentExtra(intent: Intent, content: NotificationContent): Intent {
+ // We will show the notification page for dependent urls. We can trigger a click next time
+ intent.data = Uri.parse(if (content.href.isIndependent) content.href else FbItem.NOTIFICATIONS.url)
+ bindRequest(intent, content)
+ return intent
+ }
+
+ /**
+ * Create a generic content for the provided type and user id.
+ * No content related data is added
+ */
+ fun createCommonIntent(context: Context, userId: Long): Intent {
+ val intent = Intent(context, FrostWebActivity::class.java)
+ intent.putExtra(ARG_USER_ID, userId)
+ overlayContext.put(intent)
+ return intent
+ }
+
+ /**
* Create and submit a new notification with the given [content]
*/
private fun createNotification(context: Context, content: NotificationContent): FrostNotification =
with(content) {
- val intent = Intent(context, FrostWebActivity::class.java)
- // TODO temp fix; we will show notification page for dependent urls. We can trigger a click next time
- intent.data = Uri.parse(if (href.isIndependent) href else FbItem.NOTIFICATIONS.url)
- intent.putExtra(ARG_USER_ID, data.id)
- overlayContext.put(intent)
- bindRequest(intent, content, data.cookie)
-
+ val intent = createCommonIntent(context, content.data.id)
+ putContentExtra(intent, content)
val group = "${groupPrefix}_${data.id}"
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val notifBuilder = context.frostNotification(channelId)
@@ -257,13 +276,15 @@ enum class NotificationType(
* Notification data holder
*/
data class NotificationContent(
- val data: CookieModel,
+ // TODO replace data with userId?
+ val data: CookieEntity,
val id: Long,
val href: String,
val title: String? = null, // defaults to frost title
val text: String,
val timestamp: Long,
- val profileUrl: String?
+ val profileUrl: String?,
+ val unread: Boolean
) {
val notifId = Math.abs(id.toInt())
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 b1e0ac8c..0eee5558 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
@@ -21,16 +21,19 @@ import androidx.core.app.NotificationManagerCompat
import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
-import com.pitchedapps.frost.dbflow.CookieModel
-import com.pitchedapps.frost.dbflow.loadFbCookiesSync
+import com.pitchedapps.frost.db.CookieDao
+import com.pitchedapps.frost.db.CookieEntity
+import com.pitchedapps.frost.db.selectAll
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostEvent
+import com.pitchedapps.frost.widgets.NotificationWidget
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
+import org.koin.android.ext.android.inject
/**
* Created by Allan Wang on 2017-06-14.
@@ -42,6 +45,8 @@ import kotlinx.coroutines.yield
*/
class NotificationService : BaseJobService() {
+ val cookieDao: CookieDao by inject()
+
override fun onStopJob(params: JobParameters?): Boolean {
super.onStopJob(params)
prepareFinish(true)
@@ -81,7 +86,7 @@ class NotificationService : BaseJobService() {
private suspend fun sendNotifications(params: JobParameters?): Unit = withContext(Dispatchers.Default) {
val currentId = Prefs.userId
- val cookies = loadFbCookiesSync()
+ val cookies = cookieDao.selectAll()
yield()
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
var notifCount = 0
@@ -101,13 +106,16 @@ class NotificationService : BaseJobService() {
L.i { "Sent $notifCount notifications" }
if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG)
+ if (notifCount > 0) {
+ NotificationWidget.forceUpdate(this@NotificationService)
+ }
}
/**
* Implemented fetch to also notify when an error occurs
* Also normalized the output to return the number of notifications received
*/
- private fun fetch(jobId: Int, type: NotificationType, cookie: CookieModel): Int {
+ private suspend fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int {
val count = type.fetch(this, cookie)
if (count < 0) {
if (jobId == NOTIFICATION_JOB_NOW)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt
index 20a497e3..bba2a9a1 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationUtils.kt
@@ -46,14 +46,14 @@ fun setupNotificationChannels(c: Context) {
val manager = c.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val appName = c.string(R.string.frost_name)
val msg = c.string(R.string.messages)
+ manager.createNotificationChannel(NOTIF_CHANNEL_GENERAL, appName)
+ manager.createNotificationChannel(NOTIF_CHANNEL_MESSAGES, "$appName: $msg")
manager.notificationChannels
.filter {
it.id != NOTIF_CHANNEL_GENERAL &&
it.id != NOTIF_CHANNEL_MESSAGES
}
.forEach { manager.deleteNotificationChannel(it.id) }
- manager.createNotificationChannel(NOTIF_CHANNEL_GENERAL, appName)
- manager.createNotificationChannel(NOTIF_CHANNEL_MESSAGES, "$appName: $msg")
L.d { "Created notification channels: ${manager.notificationChannels.size} channels, ${manager.notificationChannelGroups.size} groups" }
}
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 10fa5c99..b77c0f2d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Behaviour.kt
@@ -19,8 +19,10 @@ 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.facebook.FB_URL_BASE
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.REQUEST_REFRESH
+import com.pitchedapps.frost.utils.launchWebOverlay
/**
* Created by Allan Wang on 2017-06-30.
@@ -58,6 +60,13 @@ fun SettingsActivity.getBehaviourPrefs(): KPrefAdapterBuilder.() -> Unit = {
descRes = R.string.enable_pip_desc
}
+ plainText(R.string.autoplay_settings) {
+ descRes = R.string.autoplay_settings_desc
+ onClick = {
+ launchWebOverlay("${FB_URL_BASE}settings/videos/")
+ }
+ }
+
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/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt
index 0200f109..c58710b5 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt
@@ -29,14 +29,15 @@ import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.SettingsActivity
-import com.pitchedapps.frost.dbflow.NotificationModel
-import com.pitchedapps.frost.dbflow.loadFbCookiesAsync
+import com.pitchedapps.frost.db.FrostDatabase
+import com.pitchedapps.frost.db.deleteAll
import com.pitchedapps.frost.services.fetchNotifications
import com.pitchedapps.frost.services.scheduleNotifications
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostSnackbar
import com.pitchedapps.frost.utils.materialDialogThemed
import com.pitchedapps.frost.views.Keywords
+import kotlinx.coroutines.launch
/**
* Created by Allan Wang on 2017-06-29.
@@ -171,8 +172,8 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = {
if (BuildConfig.DEBUG) {
plainText(R.string.reset_notif_epoch) {
onClick = {
- loadFbCookiesAsync { cookies ->
- cookies.map { NotificationModel(it.id) }.forEach { it.save() }
+ launch {
+ FrostDatabase.get().notifDao().deleteAll()
}
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt
index f4baa242..50863e10 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt
@@ -29,8 +29,8 @@ import ca.allanwang.kau.utils.showAppInfo
import ca.allanwang.kau.utils.string
import ca.allanwang.kau.utils.toast
import com.pitchedapps.frost.R
-import com.pitchedapps.frost.dbflow.loadFbCookie
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.db.CookieEntity
+import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
/**
* Created by Allan Wang on 2017-08-04.
@@ -38,19 +38,21 @@ import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
* With reference to <a href="https://stackoverflow.com/questions/33434532/android-webview-download-files-like-browsers-do">Stack Overflow</a>
*/
fun Context.frostDownload(
+ cookie: CookieEntity,
url: String?,
- userAgent: String = USER_AGENT_BASIC,
+ userAgent: String = USER_AGENT_DESKTOP,
contentDisposition: String? = null,
mimeType: String? = null,
contentLength: Long = 0L
) {
url ?: return
- frostDownload(Uri.parse(url), userAgent, contentDisposition, mimeType, contentLength)
+ frostDownload(cookie, Uri.parse(url), userAgent, contentDisposition, mimeType, contentLength)
}
fun Context.frostDownload(
+ cookie: CookieEntity,
uri: Uri?,
- userAgent: String = USER_AGENT_BASIC,
+ userAgent: String = USER_AGENT_DESKTOP,
contentDisposition: String? = null,
mimeType: String? = null,
contentLength: Long = 0L
@@ -75,7 +77,6 @@ fun Context.frostDownload(
if (!granted) return@kauRequestPermissions
val request = DownloadManager.Request(uri)
request.setMimeType(mimeType)
- val cookie = loadFbCookie(Prefs.userId) ?: return@kauRequestPermissions
val title = URLUtil.guessFileName(uri.toString(), contentDisposition, mimeType)
request.addRequestHeader("Cookie", cookie.cookie)
request.addRequestHeader("User-Agent", userAgent)
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 8364c34e..7c8c1895 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt
@@ -50,6 +50,11 @@ object L : KauLogger("Frost", {
d(message)
}
+ inline fun _e(e: Throwable?, message: () -> Any?) {
+ if (BuildConfig.DEBUG)
+ e(e, message)
+ }
+
override fun logImpl(priority: Int, message: String?, t: Throwable?) {
if (BuildConfig.DEBUG)
super.logImpl(priority, message, t)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt
new file mode 100644
index 00000000..c0feab1e
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/TimeUtils.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.utils
+
+import android.content.Context
+import ca.allanwang.kau.utils.string
+import com.pitchedapps.frost.R
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Converts time in millis to readable date,
+ * eg Apr 24 at 7:32 PM
+ *
+ * With regards to date modifications in calendars,
+ * it appears to respect calendar rules;
+ * see https://stackoverflow.com/a/43227817/4407321
+ */
+fun Long.toReadableTime(context: Context): String {
+ val cal = Calendar.getInstance()
+ cal.timeInMillis = this
+ val timeFormatter = SimpleDateFormat.getTimeInstance(DateFormat.SHORT)
+ val time = timeFormatter.format(Date(this))
+ val day = when {
+ cal >= Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) } -> context.string(R.string.today)
+ cal >= Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -2) } -> context.string(R.string.yesterday)
+ else -> {
+ val dayFormatter = SimpleDateFormat("MMM dd", Locale.getDefault())
+ dayFormatter.format(Date(this))
+ }
+ }
+ return context.getString(R.string.time_template, day, time)
+}
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 4410b26e..76ffd8cd 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
@@ -61,14 +61,14 @@ import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.activities.TabCustomizerActivity
import com.pitchedapps.frost.activities.WebOverlayActivity
import com.pitchedapps.frost.activities.WebOverlayActivityBase
-import com.pitchedapps.frost.activities.WebOverlayBasicActivity
-import com.pitchedapps.frost.dbflow.CookieModel
+import com.pitchedapps.frost.activities.WebOverlayDesktopActivity
+import com.pitchedapps.frost.db.CookieEntity
import com.pitchedapps.frost.facebook.FACEBOOK_COM
import com.pitchedapps.frost.facebook.FBCDN_NET
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.FbUrlFormatter.Companion.VIDEO_REDIRECT
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.injectors.CssAssets
import com.pitchedapps.frost.injectors.JsAssets
@@ -103,7 +103,7 @@ internal inline val Context.ctxCoroutine: CoroutineScope
get() = this as? CoroutineScope ?: GlobalScope
inline fun <reified T : Activity> Context.launchNewTask(
- cookieList: ArrayList<CookieModel> = arrayListOf(),
+ cookieList: ArrayList<CookieEntity> = arrayListOf(),
clearStack: Boolean = false
) {
startActivity<T>(clearStack, intentBuilder = {
@@ -111,13 +111,13 @@ inline fun <reified T : Activity> Context.launchNewTask(
})
}
-fun Context.launchLogin(cookieList: ArrayList<CookieModel>, clearStack: Boolean = true) {
+fun Context.launchLogin(cookieList: ArrayList<CookieEntity>, clearStack: Boolean = true) {
if (cookieList.isNotEmpty()) launchNewTask<SelectorActivity>(cookieList, clearStack)
else launchNewTask<LoginActivity>(clearStack = clearStack)
}
-fun Activity.cookies(): ArrayList<CookieModel> {
- return intent?.getParcelableArrayListExtra<CookieModel>(EXTRA_COOKIES) ?: arrayListOf()
+fun Activity.cookies(): ArrayList<CookieEntity> {
+ return intent?.getParcelableArrayListExtra<CookieEntity>(EXTRA_COOKIES) ?: arrayListOf()
}
/**
@@ -141,7 +141,7 @@ private inline fun <reified T : WebOverlayActivityBase> Context.launchWebOverlay
fun Context.launchWebOverlay(url: String) = launchWebOverlayImpl<WebOverlayActivity>(url)
-fun Context.launchWebOverlayBasic(url: String) = launchWebOverlayImpl<WebOverlayBasicActivity>(url)
+fun Context.launchWebOverlayDesktop(url: String) = launchWebOverlayImpl<WebOverlayDesktopActivity>(url)
private fun Context.fadeBundle() = ActivityOptions.makeCustomAnimation(
this,
@@ -186,7 +186,7 @@ fun MaterialDialog.Builder.theme(): MaterialDialog.Builder {
}
fun Activity.setFrostTheme(forceTransparent: Boolean = false) {
- val isTransparent = (Color.alpha(Prefs.bgColor) != 255) || forceTransparent
+ val isTransparent = (Color.alpha(Prefs.bgColor) != 255) || (Color.alpha(Prefs.headerColor) != 255) || forceTransparent
if (Prefs.bgColor.isColorDark)
setTheme(if (isTransparent) R.style.FrostTheme_Transparent else R.style.FrostTheme)
else
@@ -393,7 +393,7 @@ fun frostJsoup(cookie: String?, url: String) =
Jsoup.connect(url).run {
if (cookie.isNullOrBlank()) this
else cookie(FACEBOOK_COM, cookie)
- }.userAgent(USER_AGENT_BASIC).get()!!
+ }.userAgent(USER_AGENT_DESKTOP).get()!!
fun Element.first(vararg select: String): Element? {
select.forEach {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
index d60ea7ed..0269b1a9 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
@@ -33,7 +33,7 @@ import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R
-import com.pitchedapps.frost.dbflow.CookieModel
+import com.pitchedapps.frost.db.CookieEntity
import com.pitchedapps.frost.facebook.profilePictureUrl
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.GlideApp
@@ -42,7 +42,7 @@ import com.pitchedapps.frost.utils.Prefs
/**
* Created by Allan Wang on 2017-06-05.
*/
-class AccountItem(val cookie: CookieModel?) : KauIItem<AccountItem, AccountItem.ViewHolder>
+class AccountItem(val cookie: CookieEntity?) : KauIItem<AccountItem, AccountItem.ViewHolder>
(R.layout.view_account, { ViewHolder(it) }, R.id.item_account) {
override fun bindView(viewHolder: ViewHolder, payloads: MutableList<Any>) {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt
index c2535940..4d88ad3d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt
@@ -32,6 +32,7 @@ import ca.allanwang.kau.utils.inflate
import ca.allanwang.kau.utils.isColorDark
import ca.allanwang.kau.utils.isGone
import ca.allanwang.kau.utils.isVisible
+import ca.allanwang.kau.utils.launchMain
import ca.allanwang.kau.utils.setIcon
import ca.allanwang.kau.utils.setMenuIcons
import ca.allanwang.kau.utils.visible
@@ -39,8 +40,11 @@ import ca.allanwang.kau.utils.withMinAlpha
import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R
+import com.pitchedapps.frost.db.FrostDatabase
+import com.pitchedapps.frost.db.currentCookie
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.ctxCoroutine
import com.pitchedapps.frost.utils.frostDownload
import kotlinx.android.synthetic.main.view_video.view.*
@@ -96,7 +100,10 @@ class FrostVideoViewer @JvmOverloads constructor(
video_toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_pip -> video.isExpanded = false
- R.id.action_download -> context.frostDownload(video.videoUri)
+ R.id.action_download -> context.ctxCoroutine.launchMain {
+ val cookie = FrostDatabase.get().cookieDao().currentCookie() ?: return@launchMain
+ context.frostDownload(cookie, video.videoUri)
+ }
}
true
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
index 0e9d94e6..1dd027fd 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt
@@ -24,21 +24,25 @@ import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import ca.allanwang.kau.utils.AnimHolder
+import ca.allanwang.kau.utils.launchMain
import com.pitchedapps.frost.contracts.FrostContentContainer
import com.pitchedapps.frost.contracts.FrostContentCore
import com.pitchedapps.frost.contracts.FrostContentParent
+import com.pitchedapps.frost.db.FrostDatabase
+import com.pitchedapps.frost.db.currentCookie
import com.pitchedapps.frost.facebook.FB_HOME_URL
import com.pitchedapps.frost.facebook.FbItem
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
-import com.pitchedapps.frost.facebook.USER_AGENT_FULL
+import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
+import com.pitchedapps.frost.facebook.USER_AGENT_MOBILE
import com.pitchedapps.frost.fragments.WebFragment
import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.ctxCoroutine
import com.pitchedapps.frost.utils.frostDownload
import com.pitchedapps.frost.web.FrostChromeClient
import com.pitchedapps.frost.web.FrostJSI
import com.pitchedapps.frost.web.FrostWebViewClient
import com.pitchedapps.frost.web.NestedWebView
-import com.pitchedapps.frost.web.shouldUseBasicAgent
+import com.pitchedapps.frost.web.shouldUseDesktopAgent
/**
* Created by Allan Wang on 2017-05-29.
@@ -66,10 +70,11 @@ class FrostWebView @JvmOverloads constructor(
@SuppressLint("SetJavaScriptEnabled")
override fun bind(container: FrostContentContainer): View {
userAgentString =
- if (parent.baseEnum == FbItem.MESSAGES || parent.baseUrl.shouldUseBasicAgent) USER_AGENT_BASIC
- else USER_AGENT_FULL
+ if (parent.baseEnum == FbItem.MESSAGES || parent.baseUrl.shouldUseDesktopAgent) USER_AGENT_DESKTOP
+ else USER_AGENT_MOBILE
with(settings) {
javaScriptEnabled = true
+ mediaPlaybackRequiresUserGesture = false // TODO check if we need this
allowFileAccess = true
textZoom = Prefs.webTextScaling
}
@@ -80,7 +85,13 @@ class FrostWebView @JvmOverloads constructor(
webChromeClient = FrostChromeClient(this)
addJavascriptInterface(FrostJSI(this), "Frost")
setBackgroundColor(Color.TRANSPARENT)
- setDownloadListener(context::frostDownload)
+ val db = FrostDatabase.get()
+ setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
+ context.ctxCoroutine.launchMain {
+ val cookie = db.cookieDao().currentCookie() ?: return@launchMain
+ context.frostDownload(cookie, url, userAgent, contentDisposition, mimetype, contentLength)
+ }
+ }
return this
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
index 90c4c7e2..c66180ed 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
@@ -24,7 +24,7 @@ import android.util.AttributeSet
import android.view.View
import android.webkit.WebView
import ca.allanwang.kau.utils.withAlpha
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.facebook.USER_AGENT_MOBILE
import com.pitchedapps.frost.injectors.CssHider
import com.pitchedapps.frost.injectors.jsInject
import com.pitchedapps.frost.utils.L
@@ -53,11 +53,12 @@ class DebugWebView @JvmOverloads constructor(
}
@SuppressLint("SetJavaScriptEnabled")
- fun setupWebview() {
+ private fun setupWebview() {
settings.javaScriptEnabled = true
- settings.userAgentString = USER_AGENT_BASIC
+ settings.userAgentString = USER_AGENT_MOBILE
setLayerType(View.LAYER_TYPE_HARDWARE, null)
webViewClient = DebugClient()
+ @Suppress("DEPRECATION")
isDrawingCacheEnabled = true
}
@@ -72,6 +73,7 @@ class DebugWebView @JvmOverloads constructor(
}
try {
output.outputStream().use {
+ @Suppress("DEPRECATION")
drawingCache.compress(Bitmap.CompressFormat.PNG, 100, it)
}
L.d { "Created screenshot at ${output.absolutePath}" }
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 50a5e2e1..0d980ba0 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt
@@ -21,7 +21,7 @@ import android.webkit.JavascriptInterface
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.contracts.MainActivityContract
import com.pitchedapps.frost.contracts.VideoViewHolder
-import com.pitchedapps.frost.dbflow.CookieModel
+import com.pitchedapps.frost.db.CookieEntity
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
@@ -44,7 +44,7 @@ class FrostJSI(val web: FrostWebView) {
private val activity: MainActivity? = context as? MainActivity
private val header: SendChannel<String>? = activity?.headerBadgeChannel
private val refresh: SendChannel<Boolean> = web.parent.refreshChannel
- private val cookies: List<CookieModel> = activity?.cookies() ?: arrayListOf()
+ private val cookies: List<CookieEntity> = activity?.cookies() ?: arrayListOf()
/**
* Attempts to load the url in an overlay
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt
index 24885fdf..81ade98f 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt
@@ -22,7 +22,7 @@ import com.pitchedapps.frost.activities.WebOverlayActivityBase
import com.pitchedapps.frost.contracts.VideoViewHolder
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
-import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.facebook.USER_AGENT_DESKTOP
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
@@ -32,7 +32,7 @@ import com.pitchedapps.frost.utils.isIndirectImageUrl
import com.pitchedapps.frost.utils.isVideoUrl
import com.pitchedapps.frost.utils.launchImageActivity
import com.pitchedapps.frost.utils.launchWebOverlay
-import com.pitchedapps.frost.utils.launchWebOverlayBasic
+import com.pitchedapps.frost.utils.launchWebOverlayDesktop
import com.pitchedapps.frost.views.FrostWebView
/**
@@ -76,15 +76,15 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean {
if (!Prefs.overlayEnabled) return false
if (context is WebOverlayActivityBase) {
L.v { "Check web request from overlay" }
- val shouldUseBasic = url.formattedFbUrl.shouldUseBasicAgent
+ val shouldUseDesktop = url.formattedFbUrl.shouldUseDesktopAgent
//already overlay; manage user agent
- if (userAgentString != USER_AGENT_BASIC && shouldUseBasic) {
- L.i { "Switch to basic agent overlay" }
- context.launchWebOverlayBasic(url)
+ if (userAgentString != USER_AGENT_DESKTOP && shouldUseDesktop) {
+ L.i { "Switch to desktop agent overlay" }
+ context.launchWebOverlayDesktop(url)
return true
}
- if (userAgentString == USER_AGENT_BASIC && !shouldUseBasic) {
- L.i { "Switch from basic agent" }
+ if (userAgentString == USER_AGENT_DESKTOP && !shouldUseDesktop) {
+ L.i { "Switch from desktop agent" }
context.launchWebOverlay(url)
return true
}
@@ -103,9 +103,9 @@ val messageWhitelist: Set<String> =
setOf(FbItem.MESSAGES, FbItem.CHAT, FbItem.FEED_MOST_RECENT, FbItem.FEED_TOP_STORIES)
.mapTo(mutableSetOf(), FbItem::url)
-val String.shouldUseBasicAgent: Boolean
+val String.shouldUseDesktopAgent: Boolean
get() = when {
- contains("story.php") -> false // do not use basic for comment section
+ contains("story.php") -> false // do not use desktop for comment section
contains("/events/") -> false // do not use for events (namely the map)
contains("/messages") -> true // must use for messages
else -> false // default to normal user agent
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 1c300b03..79c6d5ba 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
@@ -29,13 +29,12 @@ import android.webkit.WebView
import ca.allanwang.kau.utils.fadeIn
import ca.allanwang.kau.utils.isVisible
import ca.allanwang.kau.utils.launchMain
-import com.pitchedapps.frost.dbflow.CookieModel
+import com.pitchedapps.frost.db.CookieEntity
import com.pitchedapps.frost.facebook.FB_LOGIN_URL
import com.pitchedapps.frost.facebook.FB_USER_MATCHER
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.injectors.CssHider
-import com.pitchedapps.frost.injectors.JsAssets
import com.pitchedapps.frost.injectors.jsInject
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
@@ -52,7 +51,7 @@ class LoginWebView @JvmOverloads constructor(
defStyleAttr: Int = 0
) : WebView(context, attrs, defStyleAttr) {
- private val completable: CompletableDeferred<CookieModel> = CompletableDeferred()
+ private val completable: CompletableDeferred<CookieEntity> = CompletableDeferred()
private lateinit var progressCallback: (Int) -> Unit
@SuppressLint("SetJavaScriptEnabled")
@@ -63,7 +62,7 @@ class LoginWebView @JvmOverloads constructor(
webChromeClient = LoginChromeClient()
}
- suspend fun loadLogin(progressCallback: (Int) -> Unit): CompletableDeferred<CookieModel> = coroutineScope {
+ suspend fun loadLogin(progressCallback: (Int) -> Unit): CompletableDeferred<CookieEntity> = coroutineScope {
this@LoginWebView.progressCallback = progressCallback
L.d { "Begin loading login" }
launchMain {
@@ -78,18 +77,18 @@ class LoginWebView @JvmOverloads constructor(
override fun onPageFinished(view: WebView, url: String?) {
super.onPageFinished(view, url)
- val cookieModel = checkForLogin(url)
- if (cookieModel != null)
- completable.complete(cookieModel)
+ val cookie = checkForLogin(url)
+ if (cookie != null)
+ completable.complete(cookie)
if (!view.isVisible) view.fadeIn()
}
- fun checkForLogin(url: String?): CookieModel? {
+ fun checkForLogin(url: String?): CookieEntity? {
if (!url.isFacebookUrl) return null
val cookie = CookieManager.getInstance().getCookie(url) ?: return null
L.d { "Checking cookie for login" }
val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return null
- return CookieModel(id, "", cookie)
+ return CookieEntity(id, null, cookie)
}
override fun onPageCommitVisible(view: WebView, url: String?) {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt
new file mode 100644
index 00000000..594da00a
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2019 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.widgets
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService
+import androidx.annotation.ColorInt
+import androidx.annotation.DrawableRes
+import androidx.annotation.IdRes
+import ca.allanwang.kau.utils.dimenPixelSize
+import ca.allanwang.kau.utils.withAlpha
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.activities.MainActivity
+import com.pitchedapps.frost.db.NotificationDao
+import com.pitchedapps.frost.db.selectNotificationsSync
+import com.pitchedapps.frost.glide.FrostGlide
+import com.pitchedapps.frost.glide.GlideApp
+import com.pitchedapps.frost.services.NotificationContent
+import com.pitchedapps.frost.services.NotificationType
+import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.toReadableTime
+import org.koin.standalone.KoinComponent
+import org.koin.standalone.inject
+
+class NotificationWidget : AppWidgetProvider() {
+
+ override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds)
+ val type = NotificationType.GENERAL
+ val userId = Prefs.userId
+ val intent = NotificationWidgetService.createIntent(context, type, userId)
+ for (id in appWidgetIds) {
+ val views = RemoteViews(context.packageName, R.layout.widget_notifications)
+
+ views.setBackgroundColor(R.id.widget_layout_toolbar, Prefs.headerColor)
+ views.setIcon(R.id.img_frost, context, R.drawable.frost_f_24, Prefs.iconColor)
+ views.setOnClickPendingIntent(
+ R.id.img_frost,
+ PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0)
+ )
+
+ views.setBackgroundColor(R.id.widget_notification_list, Prefs.bgColor)
+ views.setRemoteAdapter(R.id.widget_notification_list, intent)
+
+ val pendingIntentTemplate = PendingIntent.getActivity(
+ context,
+ 0,
+ type.createCommonIntent(context, userId),
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ views.setPendingIntentTemplate(R.id.widget_notification_list, pendingIntentTemplate)
+
+ appWidgetManager.updateAppWidget(id, views)
+ }
+ appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_notification_list)
+ }
+
+ companion object {
+ fun forceUpdate(context: Context) {
+ val manager = AppWidgetManager.getInstance(context)
+ val ids = manager.getAppWidgetIds(ComponentName(context, NotificationWidget::class.java))
+ val intent = Intent().apply {
+ action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
+ }
+ context.sendBroadcast(intent)
+ }
+ }
+}
+
+private const val NOTIF_WIDGET_TYPE = "notif_widget_type"
+private const val NOTIF_WIDGET_USER_ID = "notif_widget_user_id"
+
+private fun RemoteViews.setBackgroundColor(@IdRes viewId: Int, @ColorInt color: Int) {
+ setInt(viewId, "setBackgroundColor", color)
+}
+
+/**
+ * Adds backward compatibility to setting tinted icons
+ */
+private fun RemoteViews.setIcon(@IdRes viewId: Int, context: Context, @DrawableRes res: Int, @ColorInt color: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val icon = Icon.createWithResource(context, res).setTint(color).setTintMode(PorterDuff.Mode.SRC_IN)
+ setImageViewIcon(viewId, icon)
+ } else {
+ val bitmap = BitmapFactory.decodeResource(context.resources, res)
+ if (bitmap != null) {
+ val paint = Paint()
+ paint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
+ val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(result)
+ canvas.drawBitmap(bitmap, 0f, 0f, paint)
+ setImageViewBitmap(viewId, result)
+ } else {
+ // Fallback to just icon
+ setImageViewResource(viewId, res)
+ }
+ }
+}
+
+class NotificationWidgetService : RemoteViewsService() {
+ override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = NotificationWidgetDataProvider(this, intent)
+
+ companion object {
+ fun createIntent(context: Context, type: NotificationType, userId: Long): Intent =
+ Intent(context, NotificationWidgetService::class.java)
+ .putExtra(NOTIF_WIDGET_TYPE, type.name)
+ .putExtra(NOTIF_WIDGET_USER_ID, userId)
+ }
+}
+
+class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : RemoteViewsService.RemoteViewsFactory,
+ KoinComponent {
+
+ private val notifDao: NotificationDao by inject()
+ @Volatile
+ private var content: List<NotificationContent> = emptyList()
+
+ private val type = NotificationType.valueOf(intent.getStringExtra(NOTIF_WIDGET_TYPE))
+
+ private val userId = intent.getLongExtra(NOTIF_WIDGET_USER_ID, -1)
+
+ private val avatarSize = context.dimenPixelSize(R.dimen.avatar_image_size)
+
+ private val glide = GlideApp.with(context).asBitmap()
+
+ private fun loadNotifications() {
+ content = notifDao.selectNotificationsSync(userId, type.channelId)
+ }
+
+ override fun onCreate() {
+ }
+
+ override fun onDataSetChanged() {
+ loadNotifications()
+ }
+
+ override fun getLoadingView(): RemoteViews? = null
+
+ override fun getItemId(position: Int): Long = content[position].id
+
+ override fun hasStableIds(): Boolean = true
+
+ override fun getViewAt(position: Int): RemoteViews {
+ val views = RemoteViews(context.packageName, R.layout.widget_notification_item)
+ val notif = content[position]
+ views.setBackgroundColor(R.id.item_frame, Prefs.nativeBgColor(notif.unread))
+ views.setTextColor(R.id.item_content, Prefs.textColor)
+ views.setTextViewText(R.id.item_content, notif.text)
+ views.setTextColor(R.id.item_date, Prefs.textColor.withAlpha(150))
+ views.setTextViewText(R.id.item_date, notif.timestamp.toReadableTime(context))
+
+ val avatar = glide.load(notif.profileUrl).transform(FrostGlide.circleCrop).submit(avatarSize, avatarSize).get()
+ views.setImageViewBitmap(R.id.item_avatar, avatar)
+ views.setOnClickFillInIntent(R.id.item_frame, type.putContentExtra(Intent(), notif))
+ return views
+ }
+
+ override fun getCount(): Int = content.size
+
+ override fun getViewTypeCount(): Int = 1
+
+ override fun onDestroy() {
+ }
+}
diff --git a/app/src/main/res/drawable/ic_fdroid_24.xml b/app/src/main/res/drawable/ic_fdroid_24.xml
new file mode 100644
index 00000000..2c718fa6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_fdroid_24.xml
@@ -0,0 +1,5 @@
+<!-- Adapted from https://gitlab.com/fdroid/artwork/blob/master/fdroid-logo-2015/default_notification.svg -->
+<vector android:height="24dp" android:viewportHeight="48"
+ android:viewportWidth="48" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="#fff" android:fillType="evenOdd" android:pathData="M2.82,1.24A1.25,1.25 0,0 0,1.8 3.27l3.6,4.66L5.2,16a3,3 0,0 0,3 3h32a3,3 0,0 0,3 -3c0,-7.38 -0.08,-7.74 -0.2,-8.07l3.6,-4.66a1.25,1.25 0,0 0,-1.06 -2.03,1.25 1.25,0 0,0 -0.93,0.5L41.2,6.18a2.99,2.99 0,0 0,-0.98 -0.18c-32.35,0 -32.68,0.07 -33,0.18L3.79,1.74a1.25,1.25 0,0 0,-0.96 -0.5zM14.58,9.75a3.38,3.38 0,0 1,3.37 3.38,3.38 3.38,0 0,1 -3.37,3.37 3.38,3.38 0,0 1,-3.38 -3.38,3.38 3.38,0 0,1 3.38,-3.37zM34.08,9.75a3.38,3.38 0,0 1,3.37 3.38,3.38 3.38,0 0,1 -3.37,3.37 3.38,3.38 0,0 1,-3.38 -3.38,3.38 3.38,0 0,1 3.38,-3.37zM8.2,20a2.97,2.97 0,0 0,-3 2.97L5.2,43a3,3 0,0 0,3 3h32a3,3 0,0 0,3 -3L43.2,22.97a2.97,2.97 0,0 0,-2.4 -2.91L8.21,20z"/>
+</vector>
diff --git a/app/src/main/res/drawable/notification_widget_preview.xml b/app/src/main/res/drawable/notification_widget_preview.xml
new file mode 100644
index 00000000..a03fd362
--- /dev/null
+++ b/app/src/main/res/drawable/notification_widget_preview.xml
@@ -0,0 +1,48 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="30dp"
+ android:height="40dp"
+ android:viewportWidth="300"
+ android:viewportHeight="400">
+ <path
+ android:pathData="M0,0h300v400H0V0z"
+ android:fillColor="#fafafa"/>
+ <path
+ android:pathData="M0,0h300v50H0V0z"
+ android:fillColor="@color/facebook_blue"/>
+ <path
+ android:pathData="M65,170a20,20 0,1 1,-40 0,20 20,0 0,1 40,0z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M85,150h184v11H85v-11z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M85,179h146v11H85v-11z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M65,95a20,20 0,1 1,-40 0,20 20,0 0,1 40,0z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M85,75h184v11H85V75z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M85,104h146v11H85v-11z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M65,245a20,20 0,1 1,-40 0,20 20,0 0,1 40,0z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M85,225h184v11H85v-11z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M85,254h146v11H85v-11z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M65,320a20,20 0,1 1,-40 0,20 20,0 0,1 40,0z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M85,300h184v11H85v-11z"
+ android:fillColor="#DE000000"/>
+ <path
+ android:pathData="M85,329h146v11H85v-11z"
+ android:fillColor="#DE000000"/>
+</vector>
diff --git a/app/src/main/res/layout/widget_notification_item.xml b/app/src/main/res/layout/widget_notification_item.xml
new file mode 100644
index 00000000..f36f2766
--- /dev/null
+++ b/app/src/main/res/layout/widget_notification_item.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/item_frame"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:selectableItemBackground"
+ android:orientation="horizontal"
+ android:paddingStart="@dimen/kau_activity_horizontal_margin"
+ android:paddingTop="@dimen/kau_activity_vertical_margin"
+ android:paddingEnd="@dimen/kau_activity_horizontal_margin"
+ android:paddingBottom="@dimen/kau_activity_vertical_margin">
+
+ <ImageView
+ android:id="@+id/item_avatar"
+ android:layout_width="@dimen/avatar_image_size"
+ android:layout_height="@dimen/avatar_image_size" />
+
+ <!--
+ Unlike the actual notification panel,
+ we do not show thumbnails, and we limit the title length
+ -->
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/kau_padding_normal"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/item_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:lines="2" />
+
+ <TextView
+ android:id="@+id/item_date"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:lines="1"
+ android:textSize="12sp" />
+
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/widget_notifications.xml b/app/src/main/res/layout/widget_notifications.xml
new file mode 100644
index 00000000..4c42be85
--- /dev/null
+++ b/app/src/main/res/layout/widget_notifications.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/widget_layout_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/widget_layout_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingStart="@dimen/kau_padding_small"
+ android:paddingEnd="@dimen/kau_padding_small">
+
+ <ImageView
+ android:id="@+id/img_frost"
+ android:layout_width="@dimen/toolbar_icon_size"
+ android:layout_height="@dimen/toolbar_icon_size"
+ android:layout_gravity="center_vertical"
+ android:layout_margin="@dimen/kau_padding_small"
+ android:background="?android:selectableItemBackgroundBorderless" />
+
+ </LinearLayout>
+
+ <ListView
+ android:id="@+id/widget_notification_list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 713bd1b4..847e74cb 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -9,4 +9,6 @@
<dimen name="tab_bar_height">50dp</dimen>
<dimen name="intro_bar_height">64dp</dimen>
<dimen name="badge_icon_size">20dp</dimen>
+
+ <dimen name="toolbar_icon_size">24dp</dimen>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5fd35613..0c2e625c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -62,4 +62,16 @@
<string name="no_new_notifications">No new notifications found</string>
+ <string name="today">Today</string>
+ <string name="yesterday">Today</string>
+ <!--
+ Template used to display human readable string;
+ For instance:
+ Today at 1:23 PM
+ Mar 13 at 9:00 AM
+
+ The first element is the day, and the second element is the time
+ -->
+ <string name="time_template">%1s at %2s</string>
+
</resources>
diff --git a/app/src/main/res/values/strings_no_translate.xml b/app/src/main/res/values/strings_no_translate.xml
index 9eb85dc2..d9aa6cee 100644
--- a/app/src/main/res/values/strings_no_translate.xml
+++ b/app/src/main/res/values/strings_no_translate.xml
@@ -6,6 +6,7 @@
<string name="translation_url" translatable="false">https://crwd.in/frost-for-facebook</string>
<string name="github_url" translatable="false">https://github.com/AllanWang/Frost-for-Facebook</string>
<string name="github_downloads_url" translatable="false">https://github.com/AllanWang/Frost-for-Facebook/releases</string>
+ <string name="fdroid_url" translatable="false">https://f-droid.org/en/packages/com.pitchedapps.frost</string>
<string name="reddit_url" translatable="false">https://www.reddit.com/r/FrostForFacebook</string>
<string name="slack_url" translatable="false">https://frost-slack.allanwang.ca</string>
<string name="xda_url" translatable="false">https://forum.xda-developers.com/android/apps-games/app-frost-facebook-t3685896</string>
diff --git a/app/src/main/res/values/strings_pref_behaviour.xml b/app/src/main/res/values/strings_pref_behaviour.xml
index 62e94112..77c35c1c 100644
--- a/app/src/main/res/values/strings_pref_behaviour.xml
+++ b/app/src/main/res/values/strings_pref_behaviour.xml
@@ -17,6 +17,8 @@
<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="enable_pip">Enable PIP</string>
<string name="enable_pip_desc">Enable picture in picture videos</string>
+ <string name="autoplay_settings">Autoplay Settings</string>
+ <string name="autoplay_settings_desc">Open Facebook\'s auto play settings. Note that it must be disabled for PIP to work.</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/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml
index c9b8477e..560b1111 100644
--- a/app/src/main/res/xml/frost_changelog.xml
+++ b/app/src/main/res/xml/frost_changelog.xml
@@ -6,10 +6,22 @@
<item text="" />
-->
- <version title="v2.2.4" />
- <item text="Show top bar to allow sharing posts" />
+ <version title="v2.3.0" />
+ <item text="Converted internals of Facebook data storage; auto migration will only work from 2.2.x to 2.3.x" />
+ <item text="Added notification widget" />
+ <item text="" />
+ <item text="" />
+ <item text="" />
+ <item text="" />
<item text="" />
<item text="" />
+ <item text="" />
+
+ <version title="v2.2.4" />
+ <item text="Show top bar to allow sharing posts" />
+ <item text="Fix unmuting videos when autoplay is enabled" />
+ <item text="Add shortcut to toggle autoplay in settings > behaviour" />
+ <item text="Update theme" />
<version title="v2.2.3" />
<item text="Add ability to hide stories" />
@@ -17,6 +29,7 @@
<item text="Apply notification keyword filter to title as well" />
<item text="Remove round icon settings as they are the default in Facebook" />
<item text="Update theme" />
+ <item text="Update translations" />
<version title="v2.2.2" />
<item text="New marketplace shortcut" />
diff --git a/app/src/main/res/xml/notification_widget_info.xml b/app/src/main/res/xml/notification_widget_info.xml
new file mode 100644
index 00000000..c14bbfb2
--- /dev/null
+++ b/app/src/main/res/xml/notification_widget_info.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+For sizing see:
+https://developer.android.com/guide/practices/ui_guidelines/widget_design.html#anatomy_determining_size
+-->
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:initialKeyguardLayout="@layout/widget_notifications"
+ android:initialLayout="@layout/widget_notifications"
+ android:minWidth="180dp"
+ android:minHeight="250dp"
+ android:previewImage="@drawable/notification_widget_preview" />
diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json
new file mode 100644
index 00000000..a0cc2c2a
--- /dev/null
+++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json
@@ -0,0 +1,195 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "fe8f5b6c27f48d7e0733ee6819f06f40",
+ "entities": [
+ {
+ "tableName": "cookies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cookie_id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`cookie_id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "cookie_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "cookie",
+ "columnName": "cookie",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "cookie_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "notifications",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "notif_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "href",
+ "columnName": "href",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "text",
+ "columnName": "text",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileUrl",
+ "columnName": "profileUrl",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unread",
+ "columnName": "unread",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "notif_id",
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_notifications_notif_id",
+ "unique": false,
+ "columnNames": [
+ "notif_id"
+ ],
+ "createSql": "CREATE INDEX `index_notifications_notif_id` ON `${TABLE_NAME}` (`notif_id`)"
+ },
+ {
+ "name": "index_notifications_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX `index_notifications_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "cookies",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "cookie_id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "frost_cache",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`id`, `type`), FOREIGN KEY(`id`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUpdated",
+ "columnName": "lastUpdated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contents",
+ "columnName": "contents",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id",
+ "type"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "cookies",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "id"
+ ],
+ "referencedColumns": [
+ "cookie_id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fe8f5b6c27f48d7e0733ee6819f06f40\")"
+ ]
+ }
+} \ No newline at end of file
diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json
new file mode 100644
index 00000000..4a523c62
--- /dev/null
+++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json
@@ -0,0 +1,40 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "ee4d2fe4052ad3a1892be17681816c2c",
+ "entities": [
+ {
+ "tableName": "frost_generic",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`type`))",
+ "fields": [
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contents",
+ "columnName": "contents",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "type"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"ee4d2fe4052ad3a1892be17681816c2c\")"
+ ]
+ }
+} \ No newline at end of file
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
index a2bafba9..0ad60126 100644
--- a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
+++ b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
@@ -27,9 +27,9 @@ import org.junit.Assume.assumeTrue
import java.io.File
import java.util.zip.ZipFile
import kotlin.test.AfterTest
-import kotlin.test.Test
import kotlin.test.BeforeTest
import kotlin.test.Ignore
+import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
diff --git a/app/src/web/assets/css/core/_core_bg.scss b/app/src/web/assets/css/core/_core_bg.scss
index 8b930922..494ee0c1 100644
--- a/app/src/web/assets/css/core/_core_bg.scss
+++ b/app/src/web/assets/css/core/_core_bg.scss
@@ -2,9 +2,9 @@
background: $background !important;
}
-body, :root, #root, #header, #MComposer, [style*="background-color"], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
+body, :root, #root, #header, #MComposer, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._3qdh, ._8ca, ._3h8i,
-._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
+._6-l ._2us7, ._6-l ._6-p:not([style*="background-image:"]), ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._24e1, ._-j7,
._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz,
._38do, .bo, .cq, ._234-, ._a-5, ._2zh4, ._15ks, ._3oyc, ._36dc, ._3iyw ._3iyx, ._6bes, ._55wo, ._4-dy,
@@ -17,7 +17,7 @@ body, :root, #root, #header, #MComposer, [style*="background-color"], ._1upc, in
._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5,
._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, ._3tl8, ._65wz, ._4edl,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._1glm,
-._ue6, ._hdn._hdn, ._6vzw, ._77xj,
+._ue6, ._hdn._hdn, ._6vzw, ._77xj, ._38nq, ._9_7, ._51li,
._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3bmj, ._5zmb, ._2x2s, ._3kac, ._3kad,
._3f50, .mentions-placeholder, .mentions, .mentions-measurer, .acg, ._59tu,
._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._1dbp, ._5zma, ._6beq, ._vi6,
@@ -27,7 +27,7 @@ body, :root, #root, #header, #MComposer, [style*="background-color"], ._1upc, in
}
//card related
-._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s {
+._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s, ._13fn {
background: $card !important;
}
@@ -58,7 +58,7 @@ body, :root, #root, #header, #MComposer, [style*="background-color"], ._1upc, in
button:not([style*=image]):not(.privacyButtons), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before,
._590n, ._4g8h, ._2cpp, ._58a0.touched:after,
.timeline .timelinePublisher, .touched, .sharerAttachment,
-.item a.primary.touched .primarywrap, ._38nq, ._537a, ._7cui,
+.item a.primary.touched .primarywrap, ._537a, ._7cui,
._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj,
.excessItem, .acr, ._5-lx, ._3g9-, ._55ws, ._6dsj ._3gin, ._69aj,
._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51,
diff --git a/app/src/web/assets/css/core/_core_border.scss b/app/src/web/assets/css/core/_core_border.scss
index 69b4ebfe..9f2bdec0 100644
--- a/app/src/web/assets/css/core/_core_border.scss
+++ b/app/src/web/assets/css/core/_core_border.scss
@@ -4,15 +4,15 @@
border-left: 1px solid $divider !important;
}
-._4_d1, ._5cni {
+._4_d1, ._5cni, ._3jcq {
border-right: 1px solid $divider !important;
}
//above see more
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._1e8h, ._z-w, ._1ha, ._1n8h ._1oby, ._5f99, ._2t39,
-._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after,
+._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after, ._46dd::before,
._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9,
-._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2,
+._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2, ._5-lw,
._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._56d8, .al {
border-top: 1px solid $divider !important;
}
@@ -24,7 +24,7 @@
.mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._38do, ._38dt,
._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .error, ._35--, ._1wev,
.jx-result, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._5rgs, ._5xuj, ._1sv1, ._idb,
-._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before,
+._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._5hu6,
._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc,
._5pz4, ._5lp4, ._5lp5, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child,
._5fjw > :first-child {
diff --git a/app/src/web/assets/css/core/_core_text.scss b/app/src/web/assets/css/core/_core_text.scss
index 49e55f59..63622610 100644
--- a/app/src/web/assets/css/core/_core_text.scss
+++ b/app/src/web/assets/css/core/_core_text.scss
@@ -1,4 +1,4 @@
-[style*="color"], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
+body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk,
.touched *, ._1_yj, ._1_yl, ._4pj9, ._2bdc, ._3qdh ._3qdn ._3qdk, ._3qdk ._48_q,
._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, .cq, ._usr,
@@ -10,15 +10,16 @@
._18qg, ._1_ac, ._529p, ._4dwt ._1vh3, ._4a5f, ._23_t, ._2rzc, ._23_s, ._2rzd,
._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._36e0, ._4-dy,
._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, .mCount, ._27vc, ._24e1, ._2rbw, ._3iyw ._3mzw,
-textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
+textarea:not([style*="color: rgb"]), ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._6--d, ._77p7, ._7h_g,
-._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg,
+._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg, ._21dc,
._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._6xqt, ._7cui,
-._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g,
-._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd,
+._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._7cdj,
+._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd, ._3gir, ._3gis,
div.sharerSelector, .footer, ._4pv_, ._1dbp, ._3kad, ._20zc, ._2i5v, ._2i5w,
a, ._5fpq, ._4gux, ._3bg5 ._52x1, ._3bg5 ._52x2, ._6dsj ._3gin, ._hdn._hdn,
-.mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw,
+.mentions-input:not([style*="color: rgb"]), .mentions-placeholder:not([style*="color: rgb"]),
+.largeStatusBox .placeHolder, .fcw, ._2rgt, ._67i4 ._5hu6 ._59tt,
._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 {
diff --git a/app/src/web/assets/css/core/core.css b/app/src/web/assets/css/core/core.css
index 1f2b886b..d9a9dfd4 100644
--- a/app/src/web/assets/css/core/core.css
+++ b/app/src/web/assets/css/core/core.css
@@ -1,4 +1,4 @@
-[style*=color], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
+body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk,
.touched *, ._1_yj, ._1_yl, ._4pj9, ._2bdc, ._3qdh ._3qdn ._3qdk, ._3qdk ._48_q,
._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, .cq, ._usr,
@@ -10,15 +10,16 @@
._18qg, ._1_ac, ._529p, ._4dwt ._1vh3, ._4a5f, ._23_t, ._2rzc, ._23_s, ._2rzd,
._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._36e0, ._4-dy,
._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, .mCount, ._27vc, ._24e1, ._2rbw, ._3iyw ._3mzw,
-textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
+textarea:not([style*="color: rgb"]), ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._6--d, ._77p7, ._7h_g,
-._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg,
+._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg, ._21dc,
._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._6xqt, ._7cui,
-._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g,
-._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd,
+._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._7cdj,
+._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd, ._3gir, ._3gis,
div.sharerSelector, .footer, ._4pv_, ._1dbp, ._3kad, ._20zc, ._2i5v, ._2i5w,
a, ._5fpq, ._4gux, ._3bg5 ._52x1, ._3bg5 ._52x2, ._6dsj ._3gin, ._hdn._hdn,
-.mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw,
+.mentions-input:not([style*="color: rgb"]), .mentions-placeholder:not([style*="color: rgb"]),
+.largeStatusBox .placeHolder, .fcw, ._2rgt, ._67i4 ._5hu6 ._59tt,
._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 {
@@ -41,9 +42,9 @@ p > a, .msg span > a {
background: #451515 !important;
}
-body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
+body, :root, #root, #header, #MComposer, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._3qdh, ._8ca, ._3h8i,
-._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
+._6-l ._2us7, ._6-l ._6-p:not([style*="background-image:"]), ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._24e1, ._-j7,
._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz,
._38do, .bo, .cq, ._234-, ._a-5, ._2zh4, ._15ks, ._3oyc, ._36dc, ._3iyw ._3iyx, ._6bes, ._55wo, ._4-dy,
@@ -56,7 +57,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5,
._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, ._3tl8, ._65wz, ._4edl,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._1glm,
-._ue6, ._hdn._hdn, ._6vzw, ._77xj,
+._ue6, ._hdn._hdn, ._6vzw, ._77xj, ._38nq, ._9_7, ._51li,
._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3bmj, ._5zmb, ._2x2s, ._3kac, ._3kad,
._3f50, .mentions-placeholder, .mentions, .mentions-measurer, .acg, ._59tu,
._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._1dbp, ._5zma, ._6beq, ._vi6,
@@ -65,7 +66,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
background: rgba(255, 0, 255, 0.02) !important;
}
-._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s {
+._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s, ._13fn {
background: #239645 !important;
}
@@ -93,7 +94,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
button:not([style*=image]):not(.privacyButtons), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before,
._590n, ._4g8h, ._2cpp, ._58a0.touched:after,
.timeline .timelinePublisher, .touched, .sharerAttachment,
-.item a.primary.touched .primarywrap, ._38nq, ._537a, ._7cui,
+.item a.primary.touched .primarywrap, ._537a, ._7cui,
._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj,
.excessItem, .acr, ._5-lx, ._3g9-, ._55ws, ._6dsj ._3gin, ._69aj,
._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51,
@@ -124,14 +125,14 @@ button ._v89 ._54k8._1fl1 {
border-left: 1px solid rgba(215, 176, 215, 0.3) !important;
}
-._4_d1, ._5cni {
+._4_d1, ._5cni, ._3jcq {
border-right: 1px solid rgba(215, 176, 215, 0.3) !important;
}
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._1e8h, ._z-w, ._1ha, ._1n8h ._1oby, ._5f99, ._2t39,
-._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after,
+._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after, ._46dd::before,
._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9,
-._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2,
+._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2, ._5-lw,
._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._56d8, .al {
border-top: 1px solid rgba(215, 176, 215, 0.3) !important;
}
@@ -143,7 +144,7 @@ button ._v89 ._54k8._1fl1 {
.mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._38do, ._38dt,
._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .error, ._35--, ._1wev,
.jx-result, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._5rgs, ._5xuj, ._1sv1, ._idb,
-._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before,
+._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._5hu6,
._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc,
._5pz4, ._5lp4, ._5lp5, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child,
._5fjw > :first-child {
diff --git a/app/src/web/assets/css/themes/custom.css b/app/src/web/assets/css/themes/custom.css
index 74ca4dfc..9d408971 100644
--- a/app/src/web/assets/css/themes/custom.css
+++ b/app/src/web/assets/css/themes/custom.css
@@ -1,4 +1,4 @@
-[style*=color], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
+body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk,
.touched *, ._1_yj, ._1_yl, ._4pj9, ._2bdc, ._3qdh ._3qdn ._3qdk, ._3qdk ._48_q,
._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, .cq, ._usr,
@@ -10,15 +10,16 @@
._18qg, ._1_ac, ._529p, ._4dwt ._1vh3, ._4a5f, ._23_t, ._2rzc, ._23_s, ._2rzd,
._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._36e0, ._4-dy,
._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, .mCount, ._27vc, ._24e1, ._2rbw, ._3iyw ._3mzw,
-textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
+textarea:not([style*="color: rgb"]), ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._6--d, ._77p7, ._7h_g,
-._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg,
+._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg, ._21dc,
._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._6xqt, ._7cui,
-._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g,
-._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd,
+._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._7cdj,
+._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd, ._3gir, ._3gis,
div.sharerSelector, .footer, ._4pv_, ._1dbp, ._3kad, ._20zc, ._2i5v, ._2i5w,
a, ._5fpq, ._4gux, ._3bg5 ._52x1, ._3bg5 ._52x2, ._6dsj ._3gin, ._hdn._hdn,
-.mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw,
+.mentions-input:not([style*="color: rgb"]), .mentions-placeholder:not([style*="color: rgb"]),
+.largeStatusBox .placeHolder, .fcw, ._2rgt, ._67i4 ._5hu6 ._59tt,
._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 {
@@ -41,9 +42,9 @@ p > a, .msg span > a {
background: $B$ !important;
}
-body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
+body, :root, #root, #header, #MComposer, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._3qdh, ._8ca, ._3h8i,
-._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
+._6-l ._2us7, ._6-l ._6-p:not([style*="background-image:"]), ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._24e1, ._-j7,
._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz,
._38do, .bo, .cq, ._234-, ._a-5, ._2zh4, ._15ks, ._3oyc, ._36dc, ._3iyw ._3iyx, ._6bes, ._55wo, ._4-dy,
@@ -56,7 +57,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5,
._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, ._3tl8, ._65wz, ._4edl,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._1glm,
-._ue6, ._hdn._hdn, ._6vzw, ._77xj,
+._ue6, ._hdn._hdn, ._6vzw, ._77xj, ._38nq, ._9_7, ._51li,
._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3bmj, ._5zmb, ._2x2s, ._3kac, ._3kad,
._3f50, .mentions-placeholder, .mentions, .mentions-measurer, .acg, ._59tu,
._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._1dbp, ._5zma, ._6beq, ._vi6,
@@ -65,7 +66,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
background: $BT$ !important;
}
-._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s {
+._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s, ._13fn {
background: $C$ !important;
}
@@ -93,7 +94,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
button:not([style*=image]):not(.privacyButtons), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before,
._590n, ._4g8h, ._2cpp, ._58a0.touched:after,
.timeline .timelinePublisher, .touched, .sharerAttachment,
-.item a.primary.touched .primarywrap, ._38nq, ._537a, ._7cui,
+.item a.primary.touched .primarywrap, ._537a, ._7cui,
._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj,
.excessItem, .acr, ._5-lx, ._3g9-, ._55ws, ._6dsj ._3gin, ._69aj,
._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51,
@@ -124,14 +125,14 @@ button ._v89 ._54k8._1fl1 {
border-left: 1px solid $D$ !important;
}
-._4_d1, ._5cni {
+._4_d1, ._5cni, ._3jcq {
border-right: 1px solid $D$ !important;
}
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._1e8h, ._z-w, ._1ha, ._1n8h ._1oby, ._5f99, ._2t39,
-._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after,
+._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after, ._46dd::before,
._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9,
-._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2,
+._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2, ._5-lw,
._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._56d8, .al {
border-top: 1px solid $D$ !important;
}
@@ -143,7 +144,7 @@ button ._v89 ._54k8._1fl1 {
.mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._38do, ._38dt,
._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .error, ._35--, ._1wev,
.jx-result, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._5rgs, ._5xuj, ._1sv1, ._idb,
-._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before,
+._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._5hu6,
._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc,
._5pz4, ._5lp4, ._5lp5, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child,
._5fjw > :first-child {
diff --git a/app/src/web/assets/css/themes/material_amoled.css b/app/src/web/assets/css/themes/material_amoled.css
index 0b9c2260..6cf12e2b 100644
--- a/app/src/web/assets/css/themes/material_amoled.css
+++ b/app/src/web/assets/css/themes/material_amoled.css
@@ -1,4 +1,4 @@
-[style*=color], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
+body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk,
.touched *, ._1_yj, ._1_yl, ._4pj9, ._2bdc, ._3qdh ._3qdn ._3qdk, ._3qdk ._48_q,
._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, .cq, ._usr,
@@ -10,15 +10,16 @@
._18qg, ._1_ac, ._529p, ._4dwt ._1vh3, ._4a5f, ._23_t, ._2rzc, ._23_s, ._2rzd,
._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._36e0, ._4-dy,
._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, .mCount, ._27vc, ._24e1, ._2rbw, ._3iyw ._3mzw,
-textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
+textarea:not([style*="color: rgb"]), ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._6--d, ._77p7, ._7h_g,
-._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg,
+._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg, ._21dc,
._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._6xqt, ._7cui,
-._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g,
-._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd,
+._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._7cdj,
+._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd, ._3gir, ._3gis,
div.sharerSelector, .footer, ._4pv_, ._1dbp, ._3kad, ._20zc, ._2i5v, ._2i5w,
a, ._5fpq, ._4gux, ._3bg5 ._52x1, ._3bg5 ._52x2, ._6dsj ._3gin, ._hdn._hdn,
-.mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw,
+.mentions-input:not([style*="color: rgb"]), .mentions-placeholder:not([style*="color: rgb"]),
+.largeStatusBox .placeHolder, .fcw, ._2rgt, ._67i4 ._5hu6 ._59tt,
._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 {
@@ -41,9 +42,9 @@ p > a, .msg span > a {
background: #000 !important;
}
-body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
+body, :root, #root, #header, #MComposer, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._3qdh, ._8ca, ._3h8i,
-._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
+._6-l ._2us7, ._6-l ._6-p:not([style*="background-image:"]), ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._24e1, ._-j7,
._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz,
._38do, .bo, .cq, ._234-, ._a-5, ._2zh4, ._15ks, ._3oyc, ._36dc, ._3iyw ._3iyx, ._6bes, ._55wo, ._4-dy,
@@ -56,7 +57,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5,
._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, ._3tl8, ._65wz, ._4edl,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._1glm,
-._ue6, ._hdn._hdn, ._6vzw, ._77xj,
+._ue6, ._hdn._hdn, ._6vzw, ._77xj, ._38nq, ._9_7, ._51li,
._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3bmj, ._5zmb, ._2x2s, ._3kac, ._3kad,
._3f50, .mentions-placeholder, .mentions, .mentions-measurer, .acg, ._59tu,
._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._1dbp, ._5zma, ._6beq, ._vi6,
@@ -65,7 +66,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
background: #000 !important;
}
-._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s {
+._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s, ._13fn {
background: rgba(0, 0, 0, 0.35) !important;
}
@@ -93,7 +94,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
button:not([style*=image]):not(.privacyButtons), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before,
._590n, ._4g8h, ._2cpp, ._58a0.touched:after,
.timeline .timelinePublisher, .touched, .sharerAttachment,
-.item a.primary.touched .primarywrap, ._38nq, ._537a, ._7cui,
+.item a.primary.touched .primarywrap, ._537a, ._7cui,
._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj,
.excessItem, .acr, ._5-lx, ._3g9-, ._55ws, ._6dsj ._3gin, ._69aj,
._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51,
@@ -124,14 +125,14 @@ button ._v89 ._54k8._1fl1 {
border-left: 1px solid rgba(255, 255, 255, 0.3) !important;
}
-._4_d1, ._5cni {
+._4_d1, ._5cni, ._3jcq {
border-right: 1px solid rgba(255, 255, 255, 0.3) !important;
}
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._1e8h, ._z-w, ._1ha, ._1n8h ._1oby, ._5f99, ._2t39,
-._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after,
+._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after, ._46dd::before,
._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9,
-._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2,
+._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2, ._5-lw,
._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._56d8, .al {
border-top: 1px solid rgba(255, 255, 255, 0.3) !important;
}
@@ -143,7 +144,7 @@ button ._v89 ._54k8._1fl1 {
.mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._38do, ._38dt,
._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .error, ._35--, ._1wev,
.jx-result, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._5rgs, ._5xuj, ._1sv1, ._idb,
-._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before,
+._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._5hu6,
._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc,
._5pz4, ._5lp4, ._5lp5, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child,
._5fjw > :first-child {
diff --git a/app/src/web/assets/css/themes/material_dark.css b/app/src/web/assets/css/themes/material_dark.css
index 4492cca4..b9799018 100644
--- a/app/src/web/assets/css/themes/material_dark.css
+++ b/app/src/web/assets/css/themes/material_dark.css
@@ -1,4 +1,4 @@
-[style*=color], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
+body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk,
.touched *, ._1_yj, ._1_yl, ._4pj9, ._2bdc, ._3qdh ._3qdn ._3qdk, ._3qdk ._48_q,
._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, .cq, ._usr,
@@ -10,15 +10,16 @@
._18qg, ._1_ac, ._529p, ._4dwt ._1vh3, ._4a5f, ._23_t, ._2rzc, ._23_s, ._2rzd,
._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._36e0, ._4-dy,
._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, .mCount, ._27vc, ._24e1, ._2rbw, ._3iyw ._3mzw,
-textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
+textarea:not([style*="color: rgb"]), ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._6--d, ._77p7, ._7h_g,
-._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg,
+._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg, ._21dc,
._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._6xqt, ._7cui,
-._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g,
-._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd,
+._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._7cdj,
+._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd, ._3gir, ._3gis,
div.sharerSelector, .footer, ._4pv_, ._1dbp, ._3kad, ._20zc, ._2i5v, ._2i5w,
a, ._5fpq, ._4gux, ._3bg5 ._52x1, ._3bg5 ._52x2, ._6dsj ._3gin, ._hdn._hdn,
-.mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw,
+.mentions-input:not([style*="color: rgb"]), .mentions-placeholder:not([style*="color: rgb"]),
+.largeStatusBox .placeHolder, .fcw, ._2rgt, ._67i4 ._5hu6 ._59tt,
._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 {
@@ -41,9 +42,9 @@ p > a, .msg span > a {
background: #303030 !important;
}
-body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
+body, :root, #root, #header, #MComposer, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._3qdh, ._8ca, ._3h8i,
-._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
+._6-l ._2us7, ._6-l ._6-p:not([style*="background-image:"]), ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._24e1, ._-j7,
._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz,
._38do, .bo, .cq, ._234-, ._a-5, ._2zh4, ._15ks, ._3oyc, ._36dc, ._3iyw ._3iyx, ._6bes, ._55wo, ._4-dy,
@@ -56,7 +57,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5,
._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, ._3tl8, ._65wz, ._4edl,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._1glm,
-._ue6, ._hdn._hdn, ._6vzw, ._77xj,
+._ue6, ._hdn._hdn, ._6vzw, ._77xj, ._38nq, ._9_7, ._51li,
._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3bmj, ._5zmb, ._2x2s, ._3kac, ._3kad,
._3f50, .mentions-placeholder, .mentions, .mentions-measurer, .acg, ._59tu,
._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._1dbp, ._5zma, ._6beq, ._vi6,
@@ -65,7 +66,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
background: #303030 !important;
}
-._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s {
+._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s, ._13fn {
background: #353535 !important;
}
@@ -93,7 +94,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
button:not([style*=image]):not(.privacyButtons), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before,
._590n, ._4g8h, ._2cpp, ._58a0.touched:after,
.timeline .timelinePublisher, .touched, .sharerAttachment,
-.item a.primary.touched .primarywrap, ._38nq, ._537a, ._7cui,
+.item a.primary.touched .primarywrap, ._537a, ._7cui,
._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj,
.excessItem, .acr, ._5-lx, ._3g9-, ._55ws, ._6dsj ._3gin, ._69aj,
._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51,
@@ -124,14 +125,14 @@ button ._v89 ._54k8._1fl1 {
border-left: 1px solid rgba(255, 255, 255, 0.3) !important;
}
-._4_d1, ._5cni {
+._4_d1, ._5cni, ._3jcq {
border-right: 1px solid rgba(255, 255, 255, 0.3) !important;
}
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._1e8h, ._z-w, ._1ha, ._1n8h ._1oby, ._5f99, ._2t39,
-._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after,
+._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after, ._46dd::before,
._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9,
-._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2,
+._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2, ._5-lw,
._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._56d8, .al {
border-top: 1px solid rgba(255, 255, 255, 0.3) !important;
}
@@ -143,7 +144,7 @@ button ._v89 ._54k8._1fl1 {
.mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._38do, ._38dt,
._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .error, ._35--, ._1wev,
.jx-result, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._5rgs, ._5xuj, ._1sv1, ._idb,
-._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before,
+._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._5hu6,
._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc,
._5pz4, ._5lp4, ._5lp5, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child,
._5fjw > :first-child {
diff --git a/app/src/web/assets/css/themes/material_glass.css b/app/src/web/assets/css/themes/material_glass.css
index 8bd6fe32..8e7656b4 100644
--- a/app/src/web/assets/css/themes/material_glass.css
+++ b/app/src/web/assets/css/themes/material_glass.css
@@ -1,4 +1,4 @@
-[style*=color], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
+body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk,
.touched *, ._1_yj, ._1_yl, ._4pj9, ._2bdc, ._3qdh ._3qdn ._3qdk, ._3qdk ._48_q,
._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, .cq, ._usr,
@@ -10,15 +10,16 @@
._18qg, ._1_ac, ._529p, ._4dwt ._1vh3, ._4a5f, ._23_t, ._2rzc, ._23_s, ._2rzd,
._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._36e0, ._4-dy,
._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, .mCount, ._27vc, ._24e1, ._2rbw, ._3iyw ._3mzw,
-textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
+textarea:not([style*="color: rgb"]), ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._6--d, ._77p7, ._7h_g,
-._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg,
+._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg, ._21dc,
._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._6xqt, ._7cui,
-._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g,
-._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd,
+._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._7cdj,
+._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd, ._3gir, ._3gis,
div.sharerSelector, .footer, ._4pv_, ._1dbp, ._3kad, ._20zc, ._2i5v, ._2i5w,
a, ._5fpq, ._4gux, ._3bg5 ._52x1, ._3bg5 ._52x2, ._6dsj ._3gin, ._hdn._hdn,
-.mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw,
+.mentions-input:not([style*="color: rgb"]), .mentions-placeholder:not([style*="color: rgb"]),
+.largeStatusBox .placeHolder, .fcw, ._2rgt, ._67i4 ._5hu6 ._59tt,
._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 {
@@ -41,9 +42,9 @@ p > a, .msg span > a {
background: rgba(0, 0, 0, 0.1) !important;
}
-body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
+body, :root, #root, #header, #MComposer, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._3qdh, ._8ca, ._3h8i,
-._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
+._6-l ._2us7, ._6-l ._6-p:not([style*="background-image:"]), ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._24e1, ._-j7,
._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz,
._38do, .bo, .cq, ._234-, ._a-5, ._2zh4, ._15ks, ._3oyc, ._36dc, ._3iyw ._3iyx, ._6bes, ._55wo, ._4-dy,
@@ -56,7 +57,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5,
._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, ._3tl8, ._65wz, ._4edl,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._1glm,
-._ue6, ._hdn._hdn, ._6vzw, ._77xj,
+._ue6, ._hdn._hdn, ._6vzw, ._77xj, ._38nq, ._9_7, ._51li,
._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3bmj, ._5zmb, ._2x2s, ._3kac, ._3kad,
._3f50, .mentions-placeholder, .mentions, .mentions-measurer, .acg, ._59tu,
._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._1dbp, ._5zma, ._6beq, ._vi6,
@@ -65,7 +66,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
background: transparent !important;
}
-._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s {
+._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s, ._13fn {
background: rgba(0, 0, 0, 0.25) !important;
}
@@ -93,7 +94,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
button:not([style*=image]):not(.privacyButtons), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before,
._590n, ._4g8h, ._2cpp, ._58a0.touched:after,
.timeline .timelinePublisher, .touched, .sharerAttachment,
-.item a.primary.touched .primarywrap, ._38nq, ._537a, ._7cui,
+.item a.primary.touched .primarywrap, ._537a, ._7cui,
._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj,
.excessItem, .acr, ._5-lx, ._3g9-, ._55ws, ._6dsj ._3gin, ._69aj,
._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51,
@@ -124,14 +125,14 @@ button ._v89 ._54k8._1fl1 {
border-left: 1px solid rgba(255, 255, 255, 0.3) !important;
}
-._4_d1, ._5cni {
+._4_d1, ._5cni, ._3jcq {
border-right: 1px solid rgba(255, 255, 255, 0.3) !important;
}
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._1e8h, ._z-w, ._1ha, ._1n8h ._1oby, ._5f99, ._2t39,
-._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after,
+._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after, ._46dd::before,
._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9,
-._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2,
+._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2, ._5-lw,
._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._56d8, .al {
border-top: 1px solid rgba(255, 255, 255, 0.3) !important;
}
@@ -143,7 +144,7 @@ button ._v89 ._54k8._1fl1 {
.mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._38do, ._38dt,
._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .error, ._35--, ._1wev,
.jx-result, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._5rgs, ._5xuj, ._1sv1, ._idb,
-._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before,
+._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._5hu6,
._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc,
._5pz4, ._5lp4, ._5lp5, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child,
._5fjw > :first-child {
diff --git a/app/src/web/assets/css/themes/material_light.css b/app/src/web/assets/css/themes/material_light.css
index f72999e0..fb738862 100644
--- a/app/src/web/assets/css/themes/material_light.css
+++ b/app/src/web/assets/css/themes/material_light.css
@@ -1,4 +1,4 @@
-[style*=color], body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
+body, input, ._42rv, ._4qau, ._dwm .descArea, ._eu5,
._1tcc, ._3g9-, ._29z_, ._3xz7, ._ib-, ._3bg5 ._56dq, ._477i, ._2vxk,
.touched *, ._1_yj, ._1_yl, ._4pj9, ._2bdc, ._3qdh ._3qdn ._3qdk, ._3qdk ._48_q,
._z-z, ._z-v, ._1e8d, ._36nl, ._36nm, ._2_11, ._2_rf, ._2ip_, ._403p, .cq, ._usr,
@@ -10,15 +10,16 @@
._18qg, ._1_ac, ._529p, ._4dwt ._1vh3, ._4a5f, ._23_t, ._2rzc, ._23_s, ._2rzd,
._5aga, ._5ag9, ._537a, .acy, ._5ro_, ._6-l ._2us7, ._4mp, ._2b08, ._36e0, ._4-dy,
._14v5 ._14v8, ._1440, ._1442, ._1448, ._4ks_, .mCount, ._27vc, ._24e1, ._2rbw, ._3iyw ._3mzw,
-textarea, ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
+textarea:not([style*="color: rgb"]), ._24pi, ._4en9, ._1kb, ._5p7j, ._2klz, ._5780, ._5781, ._5782,
._3u9u, ._3u9_, ._3u9s, ._1hcx, ._2066, ._1_-1, ._cv_, ._1nbx, ._2cuh, ._6--d, ._77p7, ._7h_g,
-._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg,
+._4ms9, ._4ms5, ._4ms6, ._31b4, ._31b5, ._5q_r, ._idb, ._38d-, ._3n8y, ._38dt, ._3oyg, ._21dc,
._27vp, ._4nwe, ._4nw9, ._27vi, .appCenterAppInfo, .appCenterPermissions, ._6xqt, ._7cui,
-._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g,
-._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd,
+._3c9l, ._3c9m, ._4jn_, ._32qt, ._3mom, ._3moo, ._-7o, ._d00, ._d01, ._559g, ._7cdj,
+._2new, .appCenterCategorySelectorButton, ._1ksq, ._1kt6, ._6ber, ._mxb, ._3oyd, ._3gir, ._3gis,
div.sharerSelector, .footer, ._4pv_, ._1dbp, ._3kad, ._20zc, ._2i5v, ._2i5w,
a, ._5fpq, ._4gux, ._3bg5 ._52x1, ._3bg5 ._52x2, ._6dsj ._3gin, ._hdn._hdn,
-.mentions-input, .mentions-placeholder, .largeStatusBox .placeHolder, .fcw,
+.mentions-input:not([style*="color: rgb"]), .mentions-placeholder:not([style*="color: rgb"]),
+.largeStatusBox .placeHolder, .fcw, ._2rgt, ._67i4 ._5hu6 ._59tt,
._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 {
@@ -41,9 +42,9 @@ p > a, .msg span > a {
background: #fafafa !important;
}
-body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
+body, :root, #root, #header, #MComposer, ._1upc, input, ._2f9r, ._59e9, ._5pz4, ._5lp4,
._5lp5, .container, .subpage, ._5n_f, #static_templates, ._22_8, ._1t4h, ._uoq, ._3qdh, ._8ca, ._3h8i,
-._6-l ._2us7, ._6-l ._6-p, ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
+._6-l ._2us7, ._6-l ._6-p:not([style*="background-image:"]), ._333v, div.sharerSelector, ._529j, ._305j, ._1pph, ._3t_l, ._4pvz,
._1g05, .acy, ._51-g, ._533c, ._ib-, .sharerAttachmentEmpty, .sharerBottomWrapper, ._24e1, ._-j7,
._3bg5 ._56do, ._5hfh, ._52e-, .mQuestionsPollResultsBar, ._5hoc, ._5oxw, ._32_4, ._1hiz,
._38do, .bo, .cq, ._234-, ._a-5, ._2zh4, ._15ks, ._3oyc, ._36dc, ._3iyw ._3iyx, ._6bes, ._55wo, ._4-dy,
@@ -56,7 +57,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
._5c9u, div._5y57::before, ._59f6._55so::before, .structuredPublisher, ._94v, ._vqv, ._5lp5,
._55wm, ._2om3, ._2ol-, ._1f9d, ._vee, ._31a-, ._3r8b, ._3r9d, ._5vq5, ._3tl8, ._65wz, ._4edl,
.acw, ._4_xl, ._1p70, ._1p70, ._1ih_, ._51v6, ._u2c, ._484w, ._3ils, ._rm7, ._32qk, ._d01, ._1glm,
-._ue6, ._hdn._hdn, ._6vzw, ._77xj,
+._ue6, ._hdn._hdn, ._6vzw, ._77xj, ._38nq, ._9_7, ._51li,
._2y60, ._5fu3, ._2foa, ._2y5_, ._38o9, ._1kb, .mAppCenterFatLabel, ._3bmj, ._5zmb, ._2x2s, ._3kac, ._3kad,
._3f50, .mentions-placeholder, .mentions, .mentions-measurer, .acg, ._59tu,
._4l9b, ._4gj3, .groupChromeView, ._i3g, ._3jcf, .error, ._1dbp, ._5zma, ._6beq, ._vi6,
@@ -65,7 +66,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
background: #fafafa !important;
}
-._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._55wo, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s {
+._31nf, ._2v9s, ._d4i, article._55wo, ._10c_, ._2jl2, ._6150, ._50mi, ._4-dw, ._4_2z, ._5m_s, ._13fn {
background: #fff !important;
}
@@ -93,7 +94,7 @@ body, :root, #root, #header, #MComposer, [style*=background-color], ._1upc, inpu
button:not([style*=image]):not(.privacyButtons), button::before, .touch ._56bt, ._56be::before, .btnS, .touch::before,
._590n, ._4g8h, ._2cpp, ._58a0.touched:after,
.timeline .timelinePublisher, .touched, .sharerAttachment,
-.item a.primary.touched .primarywrap, ._38nq, ._537a, ._7cui,
+.item a.primary.touched .primarywrap, ._537a, ._7cui,
._5xo2, ._5u5a::before, ._4u3j, ._15ks, ._5hua, ._59tt, ._41ft, .jx-tokenizer, ._55fj,
.excessItem, .acr, ._5-lx, ._3g9-, ._55ws, ._6dsj ._3gin, ._69aj,
._4e8n, ._5pxa._3uj9, ._5n_5, ._u2d, ._56bu::before, ._5h8f, ._d00, ._2066, ._2k51,
@@ -124,14 +125,14 @@ button ._v89 ._54k8._1fl1 {
border-left: 1px solid rgba(0, 0, 0, 0.3) !important;
}
-._4_d1, ._5cni {
+._4_d1, ._5cni, ._3jcq {
border-right: 1px solid rgba(0, 0, 0, 0.3) !important;
}
._1mx0, ._1rbr, ._5yt8, ._idb, ._cld, ._1e8h, ._z-w, ._1ha, ._1n8h ._1oby, ._5f99, ._2t39,
-._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after,
+._2pbp, ._5rou:first-child, ._egf:first-child, ._io2, ._3qdi ._48_m::after, ._46dd::before,
._15n_, ._3-2-, ._27ve, ._2s20, ._gui, ._2s21 > *::after, ._32qk, ._d00, ._d01, ._38o9,
-._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2,
+._3u9t, ._55fj, .mEventProfileSection.useBorder td, ._3ils, ._5as0, ._5as2, ._5-lw,
._52x1, ._3wjp, ._usq, ._2cul:before, ._13e_, .jewel .flyout, ._3bg5 ._52x6, ._56d8, .al {
border-top: 1px solid rgba(0, 0, 0, 0.3) !important;
}
@@ -143,7 +144,7 @@ button ._v89 ._54k8._1fl1 {
.mAppCenterFatLabel, .appCenterCategorySelectorButton, ._1q6v, ._5q_r, ._5yt8, ._38do, ._38dt,
._ap1, ._52x1, ._59tu, ._usq, ._13e_, ._59f6._55so::before, ._4gj3, .error, ._35--, ._1wev,
.jx-result, ._1f9d, ._vef, ._55x2 > *, .al, ._44qk, ._5rgs, ._5xuj, ._1sv1, ._idb,
-._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before,
+._5lp5, ._3-2-, ._3to6, ._ir5, ._4nw6, ._4nwh, ._27ve, div._51v6::before, ._5hu6,
._3c9h::before, ._2s20, ._gui, ._5jku, ._2foa, ._2y60, ._5fu3, ._4en9, ._1kb:not(:last-child) ._1kc,
._5pz4, ._5lp4, ._5lp5, ._5h6z, ._5h6x, ._2om4, ._5fjw > div, ._5fjv > :first-child,
._5fjw > :first-child {
diff --git a/docs/Changelog.md b/docs/Changelog.md
index 99ba22d2..39e7fa82 100644
--- a/docs/Changelog.md
+++ b/docs/Changelog.md
@@ -1,11 +1,22 @@
# Changelog
+## v2.3.0
+* Converted internals of Facebook data storage; auto migration will only work from 2.2.x to 2.3.x
+* Added notification widget
+
+## v2.2.4
+* Show top bar to allow sharing posts
+* Fix unmuting videos when autoplay is enabled
+* Add shortcut to toggle autoplay in settings > behaviour
+* Update theme
+
## v2.2.3
* Add ability to hide stories
* Remove fbclid from urls
* Apply notification keyword filter to title as well
* Remove round icon settings as they are the default in Facebook
* Update theme
+* Update translations
## v2.2.2
* New marketplace shortcut
diff --git a/gradle.properties b/gradle.properties
index 80b320d7..4c0ace29 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,7 +14,7 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro
APP_ID=Frost
APP_GROUP=com.pitchedapps
-KAU=998782a
+KAU=4.0.0
KOTLIN=1.3.21
# https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google
@@ -56,6 +56,8 @@ MATERIAL_DRAWER_KT=2.0.1
# https://github.com/square/okhttp/releases
OKHTTP=3.14.0
# http://robolectric.org/getting-started/
+# https://developer.android.com/jetpack/androidx/releases/room
+ROOM=2.1.0-alpha04
ROBOELECTRIC=4.2
# https://github.com/davemorrissey/subsampling-scale-image-view#quick-start
SCALE_IMAGE_VIEW=3.10.0