diff --git a/.gitignore b/.gitignore index 316feda..ff2b516 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -*.iml .gradle /local.properties /.idea/caches @@ -10,7 +9,6 @@ .DS_Store /build /captures -.externalNativeBuild .cxx # Created by .ignore support plugin (hsz.mobi) diff --git a/THOUGHTS.md b/THOUGHTS.md index 9a5abd9..f3b2fc2 100644 --- a/THOUGHTS.md +++ b/THOUGHTS.md @@ -112,3 +112,34 @@ generation, so I hacked it to use `"username" + i.to_s` instead of the Faker thing and it worked (spitting out tons of warnings from the cops of course). At least after all that `rails server` worked like a charm, so that's nice. + +## The Great 2020 Refresh + +The big update went rather smoothly, considering how long a year is in Android Time, +no non trivial code changes had to be made, and except for the weird exception in +[buildSrc/build.gradle.kts](buildSrc/build.gradle.kts), Kotlin is nice and up to date. + +To reflect on some earlier technology choices: + +- Still glad I chose to go with Conductor. Between easy transitions and straightforward + backstack manipulation, I feel like it hits the right balance of correct defaults + for simple situations, and power for making sure every edge case works as desired. As long + as I continue to work with Views and anything above a few screens, I feel like I will reach + for Conductor. That said, I should maybe upstream something about the view lifecycle, because + as Fragment folks have already figured out, the view lifecycle and controller lifecycle are + different, and I don't believe conductor has that built in yet. + +- SQLDelight/Coroutines/Retrofit makes data loading easy and straightforward: Just observe the + database, and when refresh events happen, hop off the main thread, download, and insert. For + paging, the Paging library does a lot of magic that means I pretty much just need a "load a page" + method, but I don't know how necessary that magic is. A nice future experiment would be to have a + list of observed pages that get rendered in the adapter, then observe more pages as the user + scrolls. + +- Multi module navigation is still something of a question mark to me, in such a small app my + approach of manual reflection is aggressively okay, not fun to write but very contained and + theoretically easy to test. I may revisit this in the future if I think of/hear about any + better approaches. + +Overall, I used Claw at least daily for the whole year before this refresh, with only one unknown +crash (and one known crash, due to lobste.rs changes), and look forward to another year of stability. \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5066ce8..e6ff7bd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,15 +4,15 @@ plugins { } android.defaultConfig.applicationId = "dev.thomasharris.claw" -// not sure why this is needed -android.packagingOptions.pickFirst("META-INF/kotlinx-coroutines-core.kotlin_module") dependencies { implementation(project(":core")) + // keep these as implementation so that build variant switching in android studio works properly implementation(project(":feature-front-page")) implementation(project(":feature-comments")) implementation(project(":feature-settings")) implementation(project(":feature-web-page")) + implementation(project(":feature-user-profile")) - debugImplementation("com.squareup.leakcanary:leakcanary-android:2.4") -} \ No newline at end of file + debugRuntimeOnly("com.squareup.leakcanary:leakcanary-android:2.6") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e7e2b99..a0ccb6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,7 +15,9 @@ android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> - + @@ -28,23 +30,14 @@ + + - - - - - - - - + diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 1f6bb29..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 0d025f9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cf..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cf..0000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 898f3ed..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index dffca36..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 64ba76f..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index dae5e08..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index e5ed465..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 14ed0af..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index b0907ca..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index d8ae031..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 2c18de9..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index beed3cd..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/build.gradle.kts b/build.gradle.kts index fd15fc6..6668b40 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,26 +1,21 @@ import dev.thomasharris.claw.build.NewModuleTask plugins { - id("com.github.ben-manes.versions") version "0.28.0" + id("com.github.ben-manes.versions") version "0.36.0" + id("org.jlleitschuh.gradle.ktlint") version "9.4.1" } -// Top-level build file where you can add configuration options common to all sub-projects/modules. - buildscript { -// ext.kotlin_version = '1.3.50' repositories { google() jcenter() - } - dependencies { - classpath("com.android.tools.build:gradle:4.0.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72") - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - classpath("com.squareup.sqldelight:gradle-plugin:1.4.0") - classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0") + dependencies { + classpath("com.android.tools.build:gradle:4.1.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21") + classpath("com.squareup.sqldelight:gradle-plugin:1.4.4") + classpath("com.github.ben-manes:gradle-versions-plugin:0.36.0") } } @@ -31,8 +26,12 @@ allprojects { } } +subprojects { + apply(plugin = "org.jlleitschuh.gradle.ktlint") +} + tasks.register("clean", Delete::class.java) { delete(rootProject.buildDir) } -tasks.register("newModule", NewModuleTask::class.java) \ No newline at end of file +tasks.register("newModule", NewModuleTask::class.java) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 9c21187..bfe202c 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -8,8 +8,23 @@ repositories { mavenCentral() } +gradlePlugin { + plugins { + create("clawPlugin") { + id = "dev.thomasharris.claw" + implementationClass = "dev.thomasharris.claw.build.ClawPlugin" + } + + create("clawAndroidPlugin") { + id = "dev.thomasharris.claw.android" + implementationClass = "dev.thomasharris.claw.build.ClawAndroidPlugin" + } + } +} + dependencies { - implementation("com.android.tools.build:gradle:4.0.1") + implementation("com.android.tools.build:gradle:4.1.1") + // CANNNOT UPDATE UNTIL kotlin-dsl PLUGIN UPDATES implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72") implementation(gradleApi()) diff --git a/buildSrc/src/main/java/dev/thomasharris/claw/build/ClawPlugin.kt b/buildSrc/src/main/java/dev/thomasharris/claw/build/ClawPlugin.kt index 5ed7ee4..1167cc4 100644 --- a/buildSrc/src/main/java/dev/thomasharris/claw/build/ClawPlugin.kt +++ b/buildSrc/src/main/java/dev/thomasharris/claw/build/ClawPlugin.kt @@ -7,7 +7,6 @@ import com.android.build.gradle.api.AndroidBasePlugin import org.gradle.api.JavaVersion import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.kotlin.dsl.findByType import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.withType @@ -16,9 +15,11 @@ import java.util.Properties /** * Technique borrowed from https://quickbirdstudios.com/blog/gradle-kotlin-buildsrc-plugin-android/ + * + * Seems to only like being the last plugin specified */ open class ClawPlugin : Plugin { - override fun apply(target: Project) = target.run { + override fun apply(target: Project): Unit = target.run { apply { plugin("java-library") plugin("kotlin") @@ -26,7 +27,6 @@ open class ClawPlugin : Plugin { } configureKotlin() - configureDependencies() } } @@ -37,14 +37,12 @@ open class ClawAndroidPlugin : Plugin { plugin("com.android.library") plugin("kotlin-android") - plugin("kotlin-android-extensions") plugin("kotlin-kapt") } configureKotlin() configureAndroid() - configureDependencies() } } @@ -57,8 +55,8 @@ internal fun Project.configureAndroid() { defaultConfig { minSdkVersion(23) targetSdkVersion(29) - versionCode = 15 - versionName = "15" + versionCode = 16 + versionName = "16" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -82,16 +80,12 @@ internal fun Project.configureAndroid() { } } - // for some reason this is still marked incubating... - // so it is overridden in gradle.properties - // buildFeatures.viewBinding = true + buildFeatures.viewBinding = true compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - - dependencies.add("implementation", "androidx.core:core-ktx:1.3.0") } extensions.findByType()?.run { @@ -108,45 +102,9 @@ internal fun Project.configureAndroid() { } } -/** - * Dependencies that everyone has, guaranteed (kotlin, dagger) - */ -internal fun Project.configureDependencies() { - dependencies.run { - add("implementation", "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72") - add("implementation", "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7") - add("implementation", "com.google.dagger:dagger:2.28.1") - add("kapt", "com.google.dagger:dagger-compiler:2.28.1") - } -} - internal fun Project.configureKotlin() = tasks.withType { kotlinOptions { jvmTarget = "1.8" + languageVersion = "1.4" } -} - -fun DependencyHandler.testing() { - add("testImplementation", "junit:junit:4.13") -} - -fun DependencyHandler.androidTesting() { - add("androidTestImplementation", "androidx.test:runner:1.2.0") - add("androidTestImplementation", "androidx.test.espresso:espresso-core:3.2.0") -} - -fun DependencyHandler.conductor() { - add("implementation", "com.bluelinelabs:conductor:3.0.0-rc6") - add("implementation", "com.bluelinelabs:conductor-archlifecycle:3.0.0-rc6") -} - -fun DependencyHandler.material() { - add("implementation", "androidx.appcompat:appcompat:1.1.0") - add("implementation", "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01") - add("implementation", "com.google.android.material:material:1.3.0-alpha01") - add("implementation", "androidx.constraintlayout:constraintlayout:1.1.3") -} - -fun DependencyHandler.coil() { - add("implementation", "io.coil-kt:coil:0.11.0") } \ No newline at end of file diff --git a/buildSrc/src/main/java/dev/thomasharris/claw/build/Deps.kt b/buildSrc/src/main/java/dev/thomasharris/claw/build/Deps.kt new file mode 100644 index 0000000..b75b543 --- /dev/null +++ b/buildSrc/src/main/java/dev/thomasharris/claw/build/Deps.kt @@ -0,0 +1,68 @@ +package dev.thomasharris.claw.build + +object Deps { + + const val material = "com.google.android.material:material:1.3.0-beta01" + const val junit = "junit:junit:4.13" + const val coil = "io.coil-kt:coil:1.1.0" + const val jsoup = "org.jsoup:jsoup:1.13.1" + const val threetenAbp = "com.jakewharton.threetenabp:threetenabp:1.3.0" + const val commonmark = "com.atlassian.commonmark:commonmark:0.16.1" + + object Kotlin { + const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21" + const val reflect = "org.jetbrains.kotlin:kotlin-reflect:1.4.21" + const val result = "com.michael-bull.kotlin-result:kotlin-result:1.1.9" + + object X { + const val coroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2" + } + } + + object Dagger { + const val dagger = "com.google.dagger:dagger:2.30.1" + const val compiler = "com.google.dagger:dagger-compiler:2.30.1" + } + + object Android { + object X { + const val coreKtx = "androidx.core:core-ktx:1.3.2" + const val appCompat = "androidx.appcompat:appcompat:1.2.0" + const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" + const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.0.4" + const val browser = "androidx.browser:browser:1.3.0" + const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" + const val pagingRuntime = "androidx.paging:paging-runtime:3.0.0-alpha11" + const val fragment = "androidx.fragment:fragment:1.3.0-rc01" + } + + object Testing { + const val runner = "androidx.test:runner:1.2.0" + const val espresso = "androidx.test.espresso:espresso-core:3.2.0" + } + } + + object Conductor { + const val conductor = "com.bluelinelabs:conductor:3.0.0-rc6" + const val lifecycle = "com.bluelinelabs:conductor-archlifecycle:3.0.0-rc6" + } + + object Square { + object Retrofit { + const val retrofit = "com.squareup.retrofit2:retrofit:2.9.0" + const val converterMoshi = "com.squareup.retrofit2:converter-moshi:2.9.0" + } + + object Moshi { + const val adapters = "com.squareup.moshi:moshi-adapters:1.11.0" + const val codegen = "com.squareup.moshi:moshi-kotlin-codegen:1.11.0" + } + + object SqlDelight { + const val androidDriver = "com.squareup.sqldelight:android-driver:1.4.4" + const val pagingExtensions = "com.squareup.sqldelight:android-paging-extensions:1.4.4" + const val coroutinesExtensions = "com.squareup.sqldelight:coroutines-extensions:1.4.4" + } + } + +} \ No newline at end of file diff --git a/buildSrc/src/main/java/dev/thomasharris/claw/build/NewModuleTask.kt b/buildSrc/src/main/java/dev/thomasharris/claw/build/NewModuleTask.kt index 0038439..44c7dab 100644 --- a/buildSrc/src/main/java/dev/thomasharris/claw/build/NewModuleTask.kt +++ b/buildSrc/src/main/java/dev/thomasharris/claw/build/NewModuleTask.kt @@ -39,11 +39,17 @@ open class NewModuleTask : DefaultTask() { // create the starting class val ktName = - "$moduleName/src/main/java/${packageStructure.split(".").joinToString(separator = "/")}.kt" + "$moduleName/src/main/java/${ + packageStructure.split(".").joinToString(separator = "/") + }.kt" with(File(project.rootDir, ktName)) { ensureParentDirsCreated() createNewFile() - writeText("package ${packageStructure.split(".").dropLast(1).joinToString(separator = ".")}") + writeText( + "package ${ + packageStructure.split(".").dropLast(1).joinToString(separator = ".") + }" + ) } // create the res folder if hasAndroid @@ -52,35 +58,48 @@ open class NewModuleTask : DefaultTask() { // create test directory val testDir = - "$moduleName/src/test/java/${packageStructure.split(".").dropLast(1).joinToString( - separator = "/" - )}" + "$moduleName/src/test/java/${ + packageStructure.split(".").dropLast(1).joinToString( + separator = "/" + ) + }" File(project.rootDir, testDir).mkdirs() // create manifest if hasAndroid if (isAndroid) with(File(project.rootDir, "$moduleName/src/main/AndroidManifest.xml")) { ensureParentDirsCreated() createNewFile() - writeText( - "" - ) + """ + " + """.trimIndent() + .let { writeText(it) } } // create build.gradle.kts with(File(project.rootDir, "$moduleName/build.gradle.kts")) { createNewFile() - appendText("import dev.thomasharris.build.testing\n\n") - appendText("plugins {\n id(\"dev.thomasharris.claw${if (isAndroid) ".android" else ""}\")\n}\n\n") - appendText("dependencies {\n testing()\n}") + """ + import dev.thomasharris.claw.build.Deps + + plugins { + id("dev.thomasharris.claw${if (isAndroid) ".android" else ""}") + } + + dependencies { + implementation(Deps.Kotlin.stdlib) + implementation(Deps.Dagger.dagger) + kapt(Deps.Dagger.compiler) + + testImplementation(Deps.junit) + } + + """.trimIndent().let { appendText(it) } } File(project.rootDir, "settings.gradle.kts").appendText("\ninclude(\":$moduleName\")") // create proguard-rules.pro - with (File(project.rootDir, "$moduleName/proguard-rules.pro")){ + with(File(project.rootDir, "$moduleName/proguard-rules.pro")) { createNewFile() appendText("# -dontobfuscate") } diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/dev.thomasharris.claw.android.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/dev.thomasharris.claw.android.properties deleted file mode 100644 index 4d4f099..0000000 --- a/buildSrc/src/main/resources/META-INF/gradle-plugins/dev.thomasharris.claw.android.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=dev.thomasharris.claw.build.ClawAndroidPlugin \ No newline at end of file diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/dev.thomasharris.claw.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/dev.thomasharris.claw.properties deleted file mode 100644 index 915d51b..0000000 --- a/buildSrc/src/main/resources/META-INF/gradle-plugins/dev.thomasharris.claw.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=dev.thomasharris.claw.build.ClawPlugin \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index d417c5d..c698e78 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,29 +1,45 @@ -import dev.thomasharris.claw.build.androidTesting -import dev.thomasharris.claw.build.coil -import dev.thomasharris.claw.build.conductor -import dev.thomasharris.claw.build.material -import dev.thomasharris.claw.build.testing +import dev.thomasharris.claw.build.Deps plugins { id("dev.thomasharris.claw.android") } dependencies { - testing() - androidTesting() - conductor() - material() - coil() + implementation(Deps.Kotlin.stdlib) - implementation("org.jetbrains.kotlin:kotlin-reflect:1.3.72") - - api("com.michael-bull.kotlin-result:kotlin-result:1.1.6") + // not sure when I can remove this, the build still complains about java.time.* + api(Deps.threetenAbp) + // api + api(Deps.Kotlin.result) api(project(":lib-lobsters")) api(project(":lib-navigator")) + api(project(":lib-better-html")) - implementation("org.jsoup:jsoup:1.13.1") + // util + implementation(Deps.Kotlin.X.coroutinesAndroid) + implementation(Deps.Android.X.coreKtx) - // not sure when I can remove this, the build still complains about java.time.* - api("com.jakewharton.threetenabp:threetenabp:1.2.4") -} \ No newline at end of file + // dagger + implementation(Deps.Dagger.dagger) + kapt(Deps.Dagger.compiler) + + // conductor + implementation(Deps.Conductor.conductor) + implementation(Deps.Conductor.lifecycle) + + // material + implementation(Deps.Android.X.appCompat) + implementation(Deps.Android.X.swipeRefreshLayout) + implementation(Deps.material) + implementation(Deps.Android.X.constraintLayout) + + // other + implementation(Deps.coil) + implementation(Deps.Kotlin.reflect) + + // testing + testImplementation(Deps.junit) + androidTestImplementation(Deps.Android.Testing.runner) + androidTestImplementation(Deps.Android.Testing.espresso) +} diff --git a/core/src/androidTest/java/dev/thomasharris/claw/core/ExampleInstrumentedTest.kt b/core/src/androidTest/java/dev/thomasharris/claw/core/ExampleInstrumentedTest.kt deleted file mode 100644 index 8b2ca27..0000000 --- a/core/src/androidTest/java/dev/thomasharris/claw/core/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.thomasharris.claw.core - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("dev.thomasharris.claw.core.test", appContext.packageName) - } -} diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index 40f6b29..af5deff 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -1,3 +1 @@ - - + diff --git a/core/src/main/java/dev/thomasharris/claw/core/ClawApplication.kt b/core/src/main/java/dev/thomasharris/claw/core/ClawApplication.kt index aeb152c..9b5cbf9 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/ClawApplication.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/ClawApplication.kt @@ -38,9 +38,8 @@ class ClawApplication : Application(), ComponentStore { if (first != null) return first - val t = factory(singletonComponent) components.add(t) return t } -} \ No newline at end of file +} diff --git a/core/src/main/java/dev/thomasharris/claw/core/HasBinding.kt b/core/src/main/java/dev/thomasharris/claw/core/HasBinding.kt index df6aafa..2ce53b6 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/HasBinding.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/HasBinding.kt @@ -8,4 +8,4 @@ interface HasBinding { fun HasBinding.withBinding(block: T.() -> Unit) { binding?.let { it.block() } -} \ No newline at end of file +} diff --git a/core/src/main/java/dev/thomasharris/claw/core/MainActivity.kt b/core/src/main/java/dev/thomasharris/claw/core/MainActivity.kt index f413c6b..f990a01 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/MainActivity.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/MainActivity.kt @@ -1,6 +1,9 @@ package dev.thomasharris.claw.core +import android.content.Intent +import android.os.Build import android.os.Bundle +import android.util.Log import android.view.View import androidx.appcompat.app.AppCompatActivity import com.bluelinelabs.conductor.Conductor @@ -11,6 +14,8 @@ import dev.thomasharris.claw.core.databinding.ActivityMainBinding import dev.thomasharris.claw.lib.navigator.Destination import dev.thomasharris.claw.lib.navigator.Navigator +private const val REQUEST_CODE = 2020_12_27 + class MainActivity : AppCompatActivity(), Navigator { private lateinit var router: Router @@ -21,19 +26,17 @@ class MainActivity : AppCompatActivity(), Navigator { setContentView(binding.root) window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or - View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - - router = Conductor.attachRouter(this, binding.conductorContainer, savedInstanceState).apply { - if (!hasRootController()) - setRoot(Destination.FrontPage.routerTransaction()) - } + View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - intent?.data?.pathSegments?.getOrNull(1)?.let { - goto( - Destination.Comments(it).routerTransaction() - .pushChangeHandler(SimpleSwapChangeHandler(false)) - ) - } + router = + Conductor.attachRouter(this, binding.conductorContainer, savedInstanceState).apply { + if (!hasRootController()) + intent + .syntheticBackstack(true) + .map(Destination::routerTransaction) + .also { it.last().pushChangeHandler(SimpleSwapChangeHandler(false)) } + .let { setBackstack(it, null) } + } } override fun goto(routerTransaction: RouterTransaction) { @@ -41,7 +44,69 @@ class MainActivity : AppCompatActivity(), Navigator { } override fun onBackPressed() { - if (!router.handleBack()) - super.onBackPressed() + if (!router.handleBack()) { + // https://issuetracker.google.com/issues/139738913 ... + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && isTaskRoot) + finishAfterTransition() + else + super.onBackPressed() + } + } + + /** + * When a new intent is received, we are already on top, + * so turn the intent into a Destination by "deeplinking" but + * *not* replacing the whole conductor backstack, to reuse the url code. + * + * If the intent is for the FrontPage, start a new MainActivity, + * instead of ignoring or, even worse, adding FrontPage as + * anything but the root of the conductor backstack + */ + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + + if (intent != null && intent.isLinkHandlingIntent()) intent + .syntheticBackstack(false) + .lastOrNull() + ?.let { dest -> + if (dest !is Destination.FrontPage) + dest.routerTransaction().let(this::goto) + else { + val explicit = Intent(this, MainActivity::class.java) + // startActivityForResult ignores singleTop :^) Can't find a better solution + startActivityForResult(explicit, REQUEST_CODE) + } + } } + + /** + * Good test area: https://lobste.rs/s/z7floj/beautiful_silent_thunderbolt_3_pc + * + * Test command: adb shell am start -a android.intent.action.VIEW -d "..." + * + * @return non empty list of destinations + */ + private fun Intent?.syntheticBackstack(isDeeplinked: Boolean): List { + if (this == null || data == null || data?.pathSegments.isNullOrEmpty()) + return listOf(Destination.FrontPage) + + return data?.pathSegments?.let { segments -> + when (segments.getOrNull(0)) { + "s" -> segments.getOrNull(1)?.let { Destination.Comments(it, isDeeplinked) } + "u" -> segments.getOrNull(1)?.let { Destination.UserProfile(it, isDeeplinked) } + else -> null + } + }.let { lastDestination -> + if (lastDestination == null) + Log.e( + this@MainActivity::class.java.simpleName, + "Failed to determine destination for $data (deeplink? $isDeeplinked)" + ) + + listOfNotNull(Destination.FrontPage, lastDestination) + } + } + + private fun Intent.isLinkHandlingIntent(): Boolean = + action == Intent.ACTION_VIEW && data?.host == "lobste.rs" } diff --git a/core/src/main/java/dev/thomasharris/claw/core/PreferencesRepository.kt b/core/src/main/java/dev/thomasharris/claw/core/PreferencesRepository.kt index 9441ebc..590236f 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/PreferencesRepository.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/PreferencesRepository.kt @@ -29,4 +29,4 @@ class PreferencesRepository @Inject constructor( AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY ) } -} \ No newline at end of file +} diff --git a/core/src/main/java/dev/thomasharris/claw/core/di/ComponentStore.kt b/core/src/main/java/dev/thomasharris/claw/core/di/ComponentStore.kt index af7d1ce..23990a8 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/di/ComponentStore.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/di/ComponentStore.kt @@ -4,4 +4,4 @@ import kotlin.reflect.KClass interface ComponentStore { fun get(obj: KClass, factory: (U) -> T): T -} \ No newline at end of file +} diff --git a/core/src/main/java/dev/thomasharris/claw/core/di/FeatureScope.kt b/core/src/main/java/dev/thomasharris/claw/core/di/FeatureScope.kt index 03f2a6c..d0bc9f8 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/di/FeatureScope.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/di/FeatureScope.kt @@ -3,4 +3,4 @@ package dev.thomasharris.claw.core.di import javax.inject.Scope @Scope -annotation class FeatureScope \ No newline at end of file +annotation class FeatureScope diff --git a/core/src/main/java/dev/thomasharris/claw/core/di/SingletonComponent.kt b/core/src/main/java/dev/thomasharris/claw/core/di/SingletonComponent.kt index 28b9df3..a668e09 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/di/SingletonComponent.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/di/SingletonComponent.kt @@ -8,6 +8,7 @@ import dagger.Provides import dev.thomasharris.claw.core.PreferencesRepository import dev.thomasharris.claw.lib.lobsters.AsyncCommentsRepository import dev.thomasharris.claw.lib.lobsters.AsyncStoryRepository +import dev.thomasharris.claw.lib.lobsters.AsyncUserRepository import dev.thomasharris.claw.lib.lobsters.Database import dev.thomasharris.claw.lib.lobsters.di.LobstersModule import javax.inject.Singleton @@ -21,6 +22,8 @@ interface SingletonComponent { val asyncCommentsRepository: AsyncCommentsRepository + val asyncUserRepository: AsyncUserRepository + fun preferencesRepository(): PreferencesRepository } @@ -34,4 +37,4 @@ class PrefsModule(private var context: Context) { Context.MODE_PRIVATE ) } -} \ No newline at end of file +} diff --git a/core/src/main/java/dev/thomasharris/claw/core/ext/ControllerExt.kt b/core/src/main/java/dev/thomasharris/claw/core/ext/ControllerExt.kt index 5767fd4..5650be8 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/ext/ControllerExt.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/ext/ControllerExt.kt @@ -9,4 +9,4 @@ inline fun Controller.getComponent(noinline factory: (Singleto @Suppress("UNCHECKED_CAST") // is checked by the as? // force unwrap the null because controllers better have an application (activity?.application as? ComponentStore)!!.get(T::class, factory) - } \ No newline at end of file + } diff --git a/core/src/main/java/dev/thomasharris/claw/core/ext/FloatExt.kt b/core/src/main/java/dev/thomasharris/claw/core/ext/FloatExt.kt index 98b79ce..b0c1d99 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/ext/FloatExt.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/ext/FloatExt.kt @@ -4,4 +4,4 @@ import android.content.Context import android.util.TypedValue fun Float.dipToPx(context: Context) = - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, context.resources.displayMetrics) \ No newline at end of file + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, context.resources.displayMetrics) diff --git a/core/src/main/java/dev/thomasharris/claw/core/ext/ViewExt.kt b/core/src/main/java/dev/thomasharris/claw/core/ext/ViewExt.kt index 4398da0..6ace36a 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/ext/ViewExt.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/ext/ViewExt.kt @@ -1,9 +1,6 @@ package dev.thomasharris.claw.core.ext import android.view.View -import androidx.appcompat.widget.Toolbar -import com.google.android.material.appbar.AppBarLayout -import dev.thomasharris.claw.core.R fun View.fade(fadeIn: Boolean) { @@ -25,17 +22,3 @@ fun View.fade(fadeIn: Boolean) { } } } - -fun Toolbar.setScrollEnabled(enabled: Boolean) { - val origScrollParams: AppBarLayout.LayoutParams = - (getTag(R.id.TOOLBAR_ORIGINAL_SCROLL_FLAGS_KEY) as? AppBarLayout.LayoutParams) - ?: (layoutParams as AppBarLayout.LayoutParams).also { - setTag(R.id.TOOLBAR_ORIGINAL_SCROLL_FLAGS_KEY, it) - } - - val noScrollParams = AppBarLayout.LayoutParams(origScrollParams).apply { - scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_NO_SCROLL - } - - layoutParams = if (enabled) origScrollParams else noScrollParams -} \ No newline at end of file diff --git a/core/src/main/java/dev/thomasharris/claw/core/ui/StoryAdditionalActionsController.kt b/core/src/main/java/dev/thomasharris/claw/core/ui/StoryAdditionalActionsController.kt new file mode 100644 index 0000000..72dba30 --- /dev/null +++ b/core/src/main/java/dev/thomasharris/claw/core/ui/StoryAdditionalActionsController.kt @@ -0,0 +1,45 @@ +package dev.thomasharris.claw.core.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import dev.thomasharris.claw.core.HasBinding +import dev.thomasharris.claw.core.R +import dev.thomasharris.claw.core.databinding.ControllerStoryAdditionalActionsBinding +import dev.thomasharris.claw.lib.navigator.Destination +import dev.thomasharris.claw.lib.navigator.goto +import dev.thomasharris.claw.lib.navigator.up + +@Suppress("unused") +class StoryAdditionalActionsController( + args: Bundle, +) : ViewLifecycleController(args), HasBinding { + override var binding: ControllerStoryAdditionalActionsBinding? = null + + private val author = args.getString("author")!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup, + savedViewState: Bundle?, + ): View { + binding = ControllerStoryAdditionalActionsBinding.inflate(inflater, container, false).apply { + scrim.setOnClickListener { + up() + } + + dialog.setOnClickListener { } + + viewProfileButton.text = + resources!!.getString(R.string.story_additional_action_view_profile, author) + + viewProfileButton.setOnClickListener { + up() + goto(Destination.UserProfile(author)) + } + } + + return requireBinding().root + } +} diff --git a/core/src/main/java/dev/thomasharris/claw/core/ui/StoryViewHolder.kt b/core/src/main/java/dev/thomasharris/claw/core/ui/StoryViewHolder.kt index b1bcdee..ac140cf 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/ui/StoryViewHolder.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/ui/StoryViewHolder.kt @@ -14,15 +14,15 @@ import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.core.text.buildSpannedString import androidx.recyclerview.widget.RecyclerView -import coil.api.load +import coil.load import coil.transform.CircleCropTransformation +import dev.thomasharris.betterhtml.PressableLinkMovementMethod +import dev.thomasharris.betterhtml.fromHtml import dev.thomasharris.claw.core.R import dev.thomasharris.claw.core.databinding.StoryViewBinding import dev.thomasharris.claw.core.ext.dipToPx import dev.thomasharris.claw.core.ext.postedAgo import dev.thomasharris.claw.core.ext.toString -import dev.thomasharris.claw.core.ui.betterhtml.PressableLinkMovementMethod -import dev.thomasharris.claw.core.ui.betterhtml.fromHtml import dev.thomasharris.claw.lib.lobsters.StoryModel import org.threeten.bp.DateTimeUtils import org.threeten.bp.Duration @@ -31,7 +31,7 @@ import java.net.URI import java.util.Date class StoryViewHolder private constructor( - private val binding: StoryViewBinding + private val binding: StoryViewBinding, ) : RecyclerView.ViewHolder(binding.root) { private val context: Context = binding.root.context @@ -40,7 +40,8 @@ class StoryViewHolder private constructor( story: StoryModel, isCompact: Boolean = true, onClickListener: ((String, String) -> Unit)? = null, - onLinkClicked: ((String) -> Unit)? = null + onLongClickListener: ((String) -> Unit)? = null, + onLinkClicked: ((String) -> Unit)? = null, ) = with(binding) { storyViewAvatar.load("https://lobste.rs/${story.avatarShortUrl}") { crossfade(true) @@ -52,13 +53,15 @@ class StoryViewHolder private constructor( append(story.title) story.tags.forEach { tag -> append(" ") - append(SpannableString(tag).apply { - val span = TagSpan( - backgroundColor = context.tagBackground(tag), - borderColor = context.tagBorder(tag) - ) - setSpan(span, 0, tag.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) - }) + append( + SpannableString(tag).apply { + val span = TagSpan( + backgroundColor = context.tagBackground(tag), + borderColor = context.tagBorder(tag) + ) + setSpan(span, 0, tag.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + ) } if (story.description.isNotBlank()) append(" ☶") } @@ -100,6 +103,12 @@ class StoryViewHolder private constructor( onClickListener(story.shortId, story.url) } + if (onLongClickListener != null) + root.setOnLongClickListener { + onLongClickListener(story.username) + true + } + val shouldShowDescription = !isCompact && story.description.isNotBlank() storyViewDescription.visibility = if (shouldShowDescription) View.VISIBLE else View.GONE if (shouldShowDescription) { @@ -130,4 +139,4 @@ fun StoryModel.shortUrl() = URI(url.trim()).host?.removePrefix("www.") fun Date.isNewUser(asOf: Instant = Instant.now()): Boolean { return Duration.between(DateTimeUtils.toInstant(this), asOf) .toDays() <= 70 // user.rb#NEW_USER_DAYS -} \ No newline at end of file +} diff --git a/core/src/main/java/dev/thomasharris/claw/core/ui/TagSpan.kt b/core/src/main/java/dev/thomasharris/claw/core/ui/TagSpan.kt index f5d4845..1f9d366 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/ui/TagSpan.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/ui/TagSpan.kt @@ -1,6 +1,10 @@ package dev.thomasharris.claw.core.ui -import android.graphics.* +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Typeface import android.text.style.ReplacementSpan import androidx.annotation.ColorInt @@ -34,14 +38,21 @@ class TagSpan( strokeWidth = 2f } - override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int { + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? + ): Int { textPaint = Paint(paint).apply { textSize *= textScale typeface = Typeface.DEFAULT color = Color.BLACK } - return paddingPx.toInt() + textPaint.measureText(text, start, end).toInt() + paddingPx.toInt() + return paddingPx.toInt() + textPaint.measureText(text, start, end) + .toInt() + paddingPx.toInt() } override fun draw( @@ -69,5 +80,4 @@ class TagSpan( canvas.drawRoundRect(rect, cornerRadiusPx, cornerRadiusPx, borderPaint) canvas.drawText(text, start, end, x + paddingPx, y.toFloat(), textPaint) } - -} \ No newline at end of file +} diff --git a/core/src/main/java/dev/thomasharris/claw/core/ui/ViewLifecycleController.kt b/core/src/main/java/dev/thomasharris/claw/core/ui/ViewLifecycleController.kt index 2dfcecb..e3bca3f 100644 --- a/core/src/main/java/dev/thomasharris/claw/core/ui/ViewLifecycleController.kt +++ b/core/src/main/java/dev/thomasharris/claw/core/ui/ViewLifecycleController.kt @@ -18,15 +18,17 @@ abstract class ViewLifecycleController(bundle: Bundle?) : LifecycleController(bu get() = _viewLifecycleOwner!! init { - addLifecycleListener(object : LifecycleListener { - override fun preCreateView(controller: Controller) { - _viewLifecycleOwner = ViewLifecycleOwner(this@ViewLifecycleController) - } + addLifecycleListener( + object : LifecycleListener { + override fun preCreateView(controller: Controller) { + _viewLifecycleOwner = ViewLifecycleOwner(this@ViewLifecycleController) + } - override fun postDestroyView(controller: Controller) { - _viewLifecycleOwner = null + override fun postDestroyView(controller: Controller) { + _viewLifecycleOwner = null + } } - }) + ) } } @@ -36,29 +38,31 @@ class ViewLifecycleOwner( private val lifecycleRegistry = LifecycleRegistry(this) init { - lifecycleController.addLifecycleListener(object : Controller.LifecycleListener { - override fun postCreateView(controller: Controller, view: View) { - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) - } + lifecycleController.addLifecycleListener( + object : Controller.LifecycleListener { + override fun postCreateView(controller: Controller, view: View) { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + } - override fun postAttach(controller: Controller, view: View) { - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) - } + override fun postAttach(controller: Controller, view: View) { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } - override fun preDetach(controller: Controller, view: View) { - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) - } + override fun preDetach(controller: Controller, view: View) { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + } - override fun preDestroyView(controller: Controller, view: View) { - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) - lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - lifecycleController.removeLifecycleListener(this) + override fun preDestroyView(controller: Controller, view: View) { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + lifecycleController.removeLifecycleListener(this) + } } - }) + ) } override fun getLifecycle(): Lifecycle { return lifecycleRegistry } -} \ No newline at end of file +} diff --git a/core/src/main/res/drawable/circle.xml b/core/src/main/res/drawable/circle.xml new file mode 100644 index 0000000..a42a101 --- /dev/null +++ b/core/src/main/res/drawable/circle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/core/src/main/res/drawable/modal_background.xml b/core/src/main/res/drawable/modal_background.xml new file mode 100644 index 0000000..625a1f8 --- /dev/null +++ b/core/src/main/res/drawable/modal_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/core/src/main/res/layout/controller_story_additional_actions.xml b/core/src/main/res/layout/controller_story_additional_actions.xml new file mode 100644 index 0000000..81f87d4 --- /dev/null +++ b/core/src/main/res/layout/controller_story_additional_actions.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/res/layout/story_view.xml b/core/src/main/res/layout/story_view.xml index c75a97e..68270a2 100644 --- a/core/src/main/res/layout/story_view.xml +++ b/core/src/main/res/layout/story_view.xml @@ -41,7 +41,7 @@ tools:text="online_username" /> - + + \ No newline at end of file diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml index 08cfa73..418231d 100644 --- a/core/src/main/res/values/colors.xml +++ b/core/src/main/res/values/colors.xml @@ -14,10 +14,12 @@ #B2CCF0 #c8c8c8 #d5d458 + #F0B2B8 #f9ddde #ddebf9 #eeeeee #fffcd7 + #f9ddde diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 5d498a7..4376832 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -7,7 +7,7 @@ - %s | by %s %s | %s + %1$s | by %2$s %3$s | %4$s %d minute ago %d minutes ago @@ -27,4 +27,7 @@ %d comment %d comments + + View profile for %1$s + Additional Actions diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index 6ca48bb..7552c60 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -17,11 +17,13 @@ @color/colorTagBorderMedia @color/colorTagBorderMeta @color/colorTagBorder + @color/colorTagBorderIsAdmin @color/colorTagBackgroundShowAskAnnounceInterview @color/colorTagBackgroundMedia @color/colorTagBackgroundMeta @color/colorTagBackground + @color/colorTagBackgroundIsAdmin