diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0ba34768..b0236e9c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,8 @@ -image: reactivecircus/android-emulator-26:latest +image: reactivecircus/android-emulator-23:latest variables: - API_LEVEL: "26" + API_LEVEL: "23" ARCH: "x86" TARGET: "default" @@ -34,25 +34,24 @@ debugTests: - ./gradlew -Pci --console=plain :app:testDebug -#emulatorTest: -# interruptible: true -# stage: test -# script: -# - sdkmanager --sdk_root=${ANDROID_HOME} "system-images;android-${API_LEVEL};${TARGET};${ARCH}" -# - echo no | avdmanager create avd --force --name "api-${API_LEVEL}" --abi "${TARGET}/${ARCH}" --package "system-images;android-${API_LEVEL};${TARGET};${ARCH}" -# - $ANDROID_HOME/emulator/emulator -avd "api-${API_LEVEL}" -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none & -# - chmod +x android-wait-for-emulator.sh -# - ./gradlew build -# - ./android-wait-for-emulator.sh -# - adb shell settings put global window_animation_scale 0.0 -# - adb shell settings put global transition_animation_scale 0.0 -# - adb shell settings put global animator_duration_scale 0.0 +emulatorTest: + interruptible: true + stage: test + script: + - echo no | avdmanager create avd --force --name "api-${API_LEVEL}" --abi "${TARGET}/${ARCH}" --package "system-images;android-${API_LEVEL};${TARGET};${ARCH}" + - $ANDROID_SDK_ROOT/emulator/emulator -avd "api-${API_LEVEL}" -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none & + - chmod +x android-wait-for-emulator.sh + - ./gradlew build + - ./android-wait-for-emulator.sh + - adb shell settings put global window_animation_scale 0.0 + - adb shell settings put global transition_animation_scale 0.0 + - adb shell settings put global animator_duration_scale 0.0 -# - ./gradlew build connectedCheck connectedDebugAndroidTest jacocoTestReport + - ./gradlew build connectedCheck connectedStagingAndroidTest jacocoTestReport -# - cat app/build/reports/jacoco/jacocoTestReport/html/index.html | grep -o 'Total[^%]*%' + - cat app/build/reports/jacoco/jacocoTestReport/html/index.html | grep -o 'Total[^%]*%' -# artifacts: -# paths: -# - ./app/build/reports/jacoco/jacocoTestReport/ -# expire_in: 1 week + artifacts: + paths: + - ./app/build/reports/jacoco/jacocoTestReport/ + expire_in: 1 week diff --git a/app/build.gradle b/app/build.gradle index 4537d64b..e1c60a52 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId "com.h.pixeldroid" minSdkVersion 23 targetSdkVersion 30 - versionCode 9 - versionName "1.0.alpha8" + versionCode 10 + versionName "1.0.alpha9" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' @@ -38,16 +38,40 @@ android { test.java.srcDirs += 'src/test/java' androidTest.java.srcDirs += 'src/androidTest/java' } + testBuildType "staging" buildTypes { debug { + + } + staging { + initWith debug testCoverageEnabled true + + // These values are first looked for in the env variables, and, if not found there, + // in the local.properties file (which is not checked into version control of course!). + + // If you are not running the integration tests, you can just set them to dummy values + // in local.properties or comment the buildConfigField lines + + def localProperties = new Properties() + if (rootProject.file("local.properties").exists()) { + localProperties.load(new FileInputStream(rootProject.file("local.properties"))) + } + + buildConfigField "String", "USER_ID", System.getenv("USER_ID") ?: localProperties['USER_ID'] + buildConfigField "String", "INSTANCE_URI", System.getenv("INSTANCE_URI") ?: localProperties['INSTANCE_URI'] + buildConfigField "String", "ACCESS_TOKEN", System.getenv("ACCESS_TOKEN") ?: localProperties['ACCESS_TOKEN'] + buildConfigField "String", "REFRESH_TOKEN", System.getenv("REFRESH_TOKEN") ?: localProperties['REFRESH_TOKEN'] + buildConfigField "String", "CLIENT_ID", System.getenv("CLIENT_ID") ?: localProperties['CLIENT_ID'] + buildConfigField "String", "CLIENT_SECRET", System.getenv("CLIENT_SECRET") ?: localProperties['CLIENT_SECRET'] } release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + minifyEnabled true + shrinkResources true + proguardFiles 'proguard-rules.pro' } } testOptions { @@ -64,7 +88,6 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" /** * AndroidX dependencies: @@ -73,32 +96,32 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.2' + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.4' implementation "androidx.browser:browser:1.3.0" implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2' - implementation 'androidx.navigation:navigation-ui-ktx:2.3.2' - implementation 'androidx.paging:paging-runtime-ktx:3.0.0-alpha12' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0' - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" + implementation 'androidx.navigation:navigation-fragment-ktx:2.3.4' + implementation 'androidx.navigation:navigation-ui-ktx:2.3.4' + implementation 'androidx.paging:paging-runtime-ktx:3.0.0-beta02' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0' + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0" implementation "androidx.annotation:annotation:1.1.0" implementation 'androidx.gridlayout:gridlayout:1.0.0' // Use the most recent version of CameraX - def cameraX_version = '1.0.0-rc01' + def cameraX_version = '1.0.0-rc03' implementation "androidx.camera:camera-core:${cameraX_version}" implementation "androidx.camera:camera-camera2:${cameraX_version}" // CameraX Lifecycle library implementation "androidx.camera:camera-lifecycle:$cameraX_version" // CameraX View class - implementation 'androidx.camera:camera-view:1.0.0-alpha20' + implementation 'androidx.camera:camera-view:1.0.0-alpha22' - def room_version = "2.3.0-alpha04" + def room_version = "2.3.0-beta03" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" @@ -109,7 +132,7 @@ dependencies { */ - implementation 'com.google.android.material:material:1.2.1' + implementation 'com.google.android.material:material:1.3.0' //Dagger (dependency injection) implementation 'com.google.dagger:dagger-android:2.30.1' @@ -124,7 +147,7 @@ dependencies { implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.20' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - implementation 'com.github.connyduck:sparkbutton:4.0.0' + implementation 'com.github.connyduck:sparkbutton:4.1.0' implementation 'info.androidhive:imagefilters:1.0.7' @@ -166,7 +189,8 @@ dependencies { */ // debugImplementation required vs testImplementation: https://issuetracker.google.com/issues/128612536 - debugImplementation("androidx.fragment:fragment-testing:1.2.5") { + //noinspection FragmentGradleConfiguration + stagingImplementation("androidx.fragment:fragment-testing:1.3.1") { exclude group:'androidx.test', module:'monitor' } @@ -195,7 +219,7 @@ tasks.withType(Test) { } -task jacocoTestReport(type: JacocoReport, dependsOn: ['connectedDebugAndroidTest', 'testDebugUnitTest', 'createDebugCoverageReport']) { +task jacocoTestReport(type: JacocoReport, dependsOn: ['connectedStagingAndroidTest', 'testStagingUnitTest', 'createStagingCoverageReport']) { reports { xml.enabled = true @@ -209,9 +233,9 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['connectedDebugAndroidTest getClassDirectories().from(files([kotlinDebugTree])) getExecutionData().from(fileTree(dir: project.buildDir, includes: [ - 'outputs/code_coverage/debugAndroidTest/connected/*coverage.ec', + 'outputs/code_coverage/stagingAndroidTest/connected/*coverage.ec', - 'jacoco/testDebugUnitTest.exec' + 'jacoco/testStagingUnitTest.exec' ])) } \ No newline at end of file diff --git a/app/licenses.yml b/app/licenses.yml index 3716a9bd..013f1a60 100644 --- a/app/licenses.yml +++ b/app/licenses.yml @@ -765,3 +765,10 @@ license: The Apache Software License, Version 2.0 licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt url: https://github.com/ongakuer/CircleIndicator +- artifact: androidx.dynamicanimation:dynamicanimation:+ + name: dynamicanimation + copyrightHolder: Google Inc + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: http://developer.android.com/tools/extras/support-library.html + diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b42451..126f20d8 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,21 +1,68 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html +# proguard file largely copied from Tusky's +# GENERAL OPTIONS (inspired from AOSP's proguard-android-optimize.txt) -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +# turn on all optimizations except those that are known to cause problems on Android +-optimizations !code/simplification/cast,!field/*,!class/merging/* +-optimizationpasses 6 +-allowaccessmodification +-dontpreverify -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-keepattributes *Annotation* -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames class * { + native ; +} +# keep setters in Views so that animations can still work. +# see http://proguard.sourceforge.net/manual/examples.html#beans +-keepclassmembers public class * extends android.view.View { + void set*(***); + *** get*(); +} +# We want to keep methods in Activity that could be used in the XML attribute onClick +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} +# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} + +# APP SPECIFIC OPTIONS + +# keep members of our model classes, they are used in json de/serialization +-keepclassmembers class com.h.pixeldroid.utils.api.objects.* { *; } + +-keep public enum com.h.pixeldroid.utils.api.objects.*$** { + **[] $VALUES; + public *; +} + +# preserve line numbers for crash reporting +-keepattributes SourceFile,LineNumberTable +-renamesourcefileattribute SourceFile + +# remove all logging from production apk +-assumenosideeffects class android.util.Log { + public static *** getStackTraceString(...); + public static *** d(...); + public static *** w(...); + public static *** v(...); + public static *** i(...); +} +-assumenosideeffects class java.lang.String { + public static java.lang.String format(...); +} + +# remove some kotlin overhead +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); + static void throwUninitializedPropertyAccessException(java.lang.String); +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/CameraTest.kt b/app/src/androidTest/java/com/h/pixeldroid/CameraTest.kt index e5ca9d0f..144cacd5 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/CameraTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/CameraTest.kt @@ -4,6 +4,7 @@ import android.Manifest import android.content.Context import android.content.Intent import android.content.Intent.ACTION_CHOOSER +import android.widget.ImageButton import androidx.fragment.app.testing.launchFragmentInContainer import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.intent.Intents @@ -14,7 +15,6 @@ import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import com.h.pixeldroid.postCreation.camera.CameraFragment import com.h.pixeldroid.testUtility.clearData import com.h.pixeldroid.testUtility.initDB -import kotlinx.android.synthetic.main.camera_ui_container.* import org.hamcrest.CoreMatchers import org.hamcrest.Matcher import org.junit.After @@ -50,9 +50,9 @@ class CameraTest { avatar_static = "some_avatar_url", isActive = true, accessToken = "token", - refreshToken = refreshToken, - clientId = clientId, - clientSecret = clientSecret + refreshToken = "refreshToken", + clientId = "clientId", + clientSecret = "clientSecret" ) ) db.close() @@ -88,23 +88,23 @@ class CameraTest { val scenario = launchFragmentInContainer() scenario.onFragment { fragment -> - fragment.photo_view_button.performClick() + fragment.view?.findViewById(R.id.photo_view_button)?.performClick() } Thread.sleep(1000) Intents.intended(expectedIntent) - - - } @Test fun switchButton() { val scenario = launchFragmentInContainer() scenario.onFragment { fragment -> - fragment.camera_switch_button.performClick() + fragment.view?.findViewById(R.id.camera_switch_button)?.performClick() } Thread.sleep(1000) + + //FIXME this assert doesn't actually do anything... + // All this test really does is make sure it doesn't crash scenario.onFragment { fragment -> assert(!fragment.isHidden) } diff --git a/app/src/androidTest/java/com/h/pixeldroid/DrawerMenuTest.kt b/app/src/androidTest/java/com/h/pixeldroid/DrawerMenuTest.kt index dde1f96c..8f60e387 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/DrawerMenuTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/DrawerMenuTest.kt @@ -12,12 +12,8 @@ import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice +import com.h.pixeldroid.testUtility.* import com.h.pixeldroid.utils.db.AppDatabase -import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity -import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity -import com.h.pixeldroid.testUtility.MockServer -import com.h.pixeldroid.testUtility.clearData -import com.h.pixeldroid.testUtility.initDB import org.junit.After import org.junit.Before import org.junit.Rule @@ -28,7 +24,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DrawerMenuTest { - private lateinit var mockServer: MockServer private lateinit var db: AppDatabase private lateinit var context: Context @@ -38,33 +33,15 @@ class DrawerMenuTest { @Before fun before(){ - mockServer = MockServer() - mockServer.start() - val baseUrl = mockServer.getUrl() - context = ApplicationProvider.getApplicationContext() db = initDB(context) db.clearAllTables() db.instanceDao().insertInstance( - InstanceDatabaseEntity( - uri = baseUrl.toString(), - title = "PixelTest" - ) + testiTestoInstance ) db.userDao().insertUser( - UserDatabaseEntity( - user_id = "123", - instance_uri = baseUrl.toString(), - username = "Testi", - display_name = "Testi Testo", - avatar_static = "some_avatar_url", - isActive = true, - accessToken = "token", - refreshToken = refreshToken, - clientId = clientId, - clientSecret = clientSecret - ) + testiTesto ) db.close() @@ -78,7 +55,6 @@ class DrawerMenuTest { @After fun after() { clearData() - mockServer.stop() } @Test @@ -132,10 +108,11 @@ class DrawerMenuTest { onView(withText(R.string.menu_account)).perform(click()) // Check that profile activity was opened. onView(withId(R.id.editButton)).check(matches(isDisplayed())) - val followersText = context.getString(R.string.nb_followers) - .format(68) + val followersText = context.resources.getQuantityString(R.plurals.nb_followers, 2, 2) onView(withText(followersText)).perform(click()) - onView(withText("Dobios")).check(matches(isDisplayed())) + + waitForView(R.id.account_entry_avatar) + onView(withText("PixelDroid Developer")).check(matches(isDisplayed())) } @Test @@ -144,10 +121,11 @@ class DrawerMenuTest { onView(withText(R.string.menu_account)).perform(click()) // Check that profile activity was opened. onView(withId(R.id.editButton)).check(matches(isDisplayed())) - val followingText = context.getString(R.string.nb_following) - .format(27) + val followingText = context.resources.getQuantityString(R.plurals.nb_following, 3, 3) onView(withText(followingText)).perform(click()) - onView(withText("Dobios")).check(matches(isDisplayed())) + + waitForView(R.id.account_entry_avatar) + onView(withText("@User 1")).check(matches(isDisplayed())) } /*@Test @@ -162,31 +140,38 @@ class DrawerMenuTest { fun clickFollowers() { // Open My Profile from drawer onView(withText(R.string.menu_account)).perform(click()) - Thread.sleep(1000) + + waitForView(R.id.nbFollowersTextView) // Open followers list onView(withId(R.id.nbFollowersTextView)).perform(click()) - Thread.sleep(1000) - // Open follower's profile - onView(withText("ete2")).perform(click()) - Thread.sleep(1000) - onView(withId(R.id.accountNameTextView)).check(matches(withText("Christian"))) + waitForView(R.id.account_entry_avatar) + + // Open follower's profile + onView(withText("@pixeldroid")).perform(click()) + + waitForView(R.id.profilePictureImageView) + + onView(withId(R.id.accountNameTextView)).check(matches(withText("PixelDroid Developer"))) } @Test fun clickFollowing() { // Open My Profile from drawer onView(withText(R.string.menu_account)).perform(click()) - Thread.sleep(1000) + waitForView(R.id.nbFollowersTextView) + // Open followers list onView(withId(R.id.nbFollowingTextView)).perform(click()) - Thread.sleep(1000) - // Open following's profile - onView(withText("Dobios")).perform(click()) - Thread.sleep(1000) - onView(withId(R.id.accountNameTextView)).check(matches(withText("Andrew Dobis"))) + waitForView(R.id.account_entry_avatar) + + // Open following's profile + onView(withText("@user2")).perform(click()) + waitForView(R.id.profilePictureImageView) + + onView(withId(R.id.accountNameTextView)).check(matches(withText("User 2"))) } @Test diff --git a/app/src/androidTest/java/com/h/pixeldroid/EditPhotoTest.kt b/app/src/androidTest/java/com/h/pixeldroid/EditPhotoTest.kt index 356cbd3b..c5868a97 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/EditPhotoTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/EditPhotoTest.kt @@ -22,14 +22,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import com.google.android.material.tabs.TabLayout -import com.h.pixeldroid.adapters.ThumbnailAdapter +import com.h.pixeldroid.postCreation.photoEdit.PhotoEditActivity +import com.h.pixeldroid.postCreation.photoEdit.ThumbnailAdapter import com.h.pixeldroid.settings.AboutActivity -import com.h.pixeldroid.testUtility.CustomMatchers import com.h.pixeldroid.testUtility.clearData -import junit.framework.Assert.assertTrue -import kotlinx.android.synthetic.main.fragment_edit_image.* +import com.h.pixeldroid.testUtility.clickChildViewWithId +import com.h.pixeldroid.testUtility.slowSwipeLeft +import com.h.pixeldroid.testUtility.waitForView import org.hamcrest.CoreMatchers.allOf import org.junit.* +import org.junit.Assert.assertTrue import org.junit.rules.Timeout import org.junit.runner.RunWith import java.io.File @@ -72,7 +74,7 @@ class EditPhotoTest { activityScenario = ActivityScenario.launch(intent).onActivity{a -> activity = a} - Thread.sleep(1000) + waitForView(R.id.coordinator_edit) } @After @@ -119,14 +121,15 @@ class EditPhotoTest { @Test fun FiltersIsSwipeableAndClickeable() { + waitForView(R.id.thumbnail) Espresso.onView(withId(R.id.recycler_view)) - .perform(actionOnItemAtPosition(1, CustomMatchers.clickChildViewWithId(R.id.thumbnail))) + .perform(actionOnItemAtPosition(1, clickChildViewWithId(R.id.thumbnail))) Thread.sleep(1000) Espresso.onView(withId(R.id.recycler_view)) - .perform(actionOnItemAtPosition(1, CustomMatchers.slowSwipeLeft(false))) + .perform(actionOnItemAtPosition(1, slowSwipeLeft(false))) Thread.sleep(1000) Espresso.onView(withId(R.id.recycler_view)) - .perform(actionOnItemAtPosition(5, CustomMatchers.clickChildViewWithId(R.id.thumbnail))) + .perform(actionOnItemAtPosition(5, clickChildViewWithId(R.id.thumbnail))) Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed())) } @@ -134,16 +137,16 @@ class EditPhotoTest { fun BrightnessSaturationContrastTest() { Espresso.onView(withId(R.id.tabs)).perform(selectTabAtPosition(1)) - Thread.sleep(1000) + waitForView(R.id.seekbar_brightness) var change = 5 Espresso.onView(withId(R.id.seekbar_brightness)).perform(setProgress(change)) Espresso.onView(withId(R.id.seekbar_contrast)).perform(setProgress(change)) Espresso.onView(withId(R.id.seekbar_saturation)).perform(setProgress(change)) - Assert.assertEquals(change, activity.seekbar_brightness.progress) - Assert.assertEquals(change, activity.seekbar_contrast.progress) - Assert.assertEquals(change, activity.seekbar_saturation.progress) + Assert.assertEquals(change, activity.findViewById(R.id.seekbar_brightness).progress) + Assert.assertEquals(change, activity.findViewById(R.id.seekbar_contrast).progress) + Assert.assertEquals(change, activity.findViewById(R.id.seekbar_saturation).progress) Thread.sleep(1000) @@ -152,28 +155,32 @@ class EditPhotoTest { Espresso.onView(withId(R.id.seekbar_contrast)).perform(setProgress(change)) Espresso.onView(withId(R.id.seekbar_saturation)).perform(setProgress(change)) - Assert.assertEquals(change, activity.seekbar_brightness.progress) - Assert.assertEquals(change, activity.seekbar_contrast.progress) - Assert.assertEquals(change, activity.seekbar_saturation.progress) + Assert.assertEquals(change, activity.findViewById(R.id.seekbar_brightness).progress) + Assert.assertEquals(change, activity.findViewById(R.id.seekbar_contrast).progress) + Assert.assertEquals(change, activity.findViewById(R.id.seekbar_saturation).progress) } @Test fun saveButton() { + // The save button saves the edits and goes back to the post creation activity. Espresso.onView(withId(R.id.action_save)).perform(click()) - Espresso.onView(withId(com.google.android.material.R.id.snackbar_text)) - .check(matches(withText(R.string.save_image_success))) + Thread.sleep(1000) + assertTrue(activityScenario.state == Lifecycle.State.DESTROYED) } @Test fun backButton() { Espresso.onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click()) + Thread.sleep(1000) assertTrue(activityScenario.state == Lifecycle.State.DESTROYED) } @Test fun croppingIsPossible() { Espresso.onView(withId(R.id.cropImageButton)).perform(click()) - Thread.sleep(1000) + + waitForView(R.id.menu_crop) + Espresso.onView(withId(R.id.menu_crop)).perform(click()) Espresso.onView(withId(R.id.image_preview)).check(matches(isDisplayed())) } @@ -181,7 +188,7 @@ class EditPhotoTest { @Test fun alreadyUploadingDialog() { activityScenario.onActivity { a -> a.saving = true } - Espresso.onView(withId(R.id.action_upload)).perform(click()) + Espresso.onView(withId(R.id.action_save)).perform(click()) Thread.sleep(1000) Espresso.onView(withText(R.string.busy_dialog_text)).check(matches(isDisplayed())) } diff --git a/app/src/androidTest/java/com/h/pixeldroid/HomeFeedTest.kt b/app/src/androidTest/java/com/h/pixeldroid/HomeFeedTest.kt index 0f188bac..d97d376d 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/HomeFeedTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/HomeFeedTest.kt @@ -10,21 +10,11 @@ import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.h.pixeldroid.utils.db.AppDatabase -import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity -import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import com.h.pixeldroid.posts.StatusViewHolder -import com.h.pixeldroid.testUtility.CustomMatchers.Companion.atPosition -import com.h.pixeldroid.testUtility.CustomMatchers.Companion.clickChildViewWithId -import com.h.pixeldroid.testUtility.CustomMatchers.Companion.first -import com.h.pixeldroid.testUtility.CustomMatchers.Companion.getText -import com.h.pixeldroid.testUtility.CustomMatchers.Companion.second -import com.h.pixeldroid.testUtility.CustomMatchers.Companion.slowSwipeUp -import com.h.pixeldroid.testUtility.CustomMatchers.Companion.typeTextInViewWithId -import com.h.pixeldroid.testUtility.MockServer -import com.h.pixeldroid.testUtility.clearData -import com.h.pixeldroid.testUtility.initDB +import com.h.pixeldroid.testUtility.* import org.hamcrest.CoreMatchers.not import org.junit.After import org.junit.Before @@ -37,7 +27,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class HomeFeedTest { - private lateinit var mockServer: MockServer private lateinit var activityScenario: ActivityScenario private lateinit var db: AppDatabase private lateinit var context: Context @@ -47,31 +36,14 @@ class HomeFeedTest { @Before fun before(){ - mockServer = MockServer() - mockServer.start() - val baseUrl = mockServer.getUrl() context = ApplicationProvider.getApplicationContext() db = initDB(context) db.clearAllTables() db.instanceDao().insertInstance( - InstanceDatabaseEntity( - uri = baseUrl.toString(), - title = "PixelTest" - ) + testiTestoInstance ) db.userDao().insertUser( - UserDatabaseEntity( - user_id = "123", - instance_uri = baseUrl.toString(), - username = "Testi", - display_name = "Testi Testo", - avatar_static = "some_avatar_url", - isActive = true, - accessToken = "token", - refreshToken = refreshToken, - clientId = clientId, - clientSecret = clientSecret - ) + testiTesto ) db.close() activityScenario = ActivityScenario.launch(MainActivity::class.java) @@ -79,21 +51,59 @@ class HomeFeedTest { @After fun after() { clearData() - mockServer.stop() } @Test fun clickingTabOnAlbumShowsNextPhoto() { //Wait for the feed to load - Thread.sleep(1000) + waitForView(R.id.postPager) activityScenario.onActivity { a -> run { //Pick the second photo - a.findViewById(R.id.postTabs).getTabAt(1)?.select() + a.findViewById(R.id.postPager).currentItem = 2 } } - onView(first(withId(R.id.postTabs))).check(matches(isDisplayed())) + onView(first(withId(R.id.postPager))).check(matches(isDisplayed())) + } +/* + @Test + fun clickingReblogButtonWorks() { + onView(withId(R.id.list)) + .perform(actionOnItemAtPosition + (0, clickChildViewWithId(R.id.reblogger))) + onView(withId(R.id.list)) + .perform(actionOnItemAtPosition + (0, clickChildViewWithId(R.id.reblogger))) + onView(first(withId(R.id.nshares))) + .check(matches(withText(getText(first(withId(R.id.nshares)))))) + } + + @Test + fun doubleTapLikerWorks() { + Thread.sleep(1000) + //Get initial like count + val likes = getText(first(withId(R.id.nlikes))) + val nLikes = likes!!.split(" ")[0].toInt() + + //Remove sensitive media warning + onView(withId(R.id.list)) + .perform(actionOnItemAtPosition + (0, clickChildViewWithId(R.id.sensitiveWarning))) + Thread.sleep(100) + + //Like the post + onView(withId(R.id.list)) + .perform(actionOnItemAtPosition + (0, clickChildViewWithId(R.id.postPicture))) + onView(withId(R.id.list)) + .perform(actionOnItemAtPosition + (0, clickChildViewWithId(R.id.postPicture))) + //... + Thread.sleep(100) + + //Profit + onView(first(withId(R.id.nlikes))).check(matches((withText("${nLikes + 1} Likes")))) } @Test @@ -114,10 +124,12 @@ class HomeFeedTest { actionOnItemAtPosition(2, clickChildViewWithId(R.id.liker)) ) onView((withId(R.id.list))).check(matches(isDisplayed())) - } + }*/ @Test fun clickingUsernameOpensProfile() { + waitForView(R.id.username) + onView(withId(R.id.list)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.username)) ) @@ -126,26 +138,18 @@ class HomeFeedTest { @Test fun clickingProfilePicOpensProfile() { + waitForView(R.id.profilePic) + onView(withId(R.id.list)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.profilePic)) ) onView(withId(R.id.accountNameTextView)).check(matches(isDisplayed())) } - @Test - fun clickingReblogButtonWorks() { - onView(withId(R.id.list)) - .perform(actionOnItemAtPosition - (0, clickChildViewWithId(R.id.reblogger))) - onView(withId(R.id.list)) - .perform(actionOnItemAtPosition - (0, clickChildViewWithId(R.id.reblogger))) - onView(first(withId(R.id.nshares))) - .check(matches(withText(getText(first(withId(R.id.nshares)))))) - } - @Test fun clickingMentionOpensProfile() { + waitForView(R.id.description) + onView(withId(R.id.list)).perform( actionOnItemAtPosition(0, clickChildViewWithId(R.id.description)) ) @@ -159,7 +163,7 @@ class HomeFeedTest { ) onView(withId(R.id.list)).check(matches(isDisplayed())) } -*/ + @Test fun clickingCommentButtonOpensCommentSection() { @@ -211,20 +215,17 @@ class HomeFeedTest { Thread.sleep(1000) onView(first(withId(R.id.commentContainer))) .check(matches(hasDescendant(withId(R.id.comment)))) - } + }*/ @Test fun performClickOnSensitiveWarning() { - onView(withId(R.id.list)).perform(scrollToPosition(1)) - Thread.sleep(1000) - + waitForView(R.id.username) + onView(second(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - Thread.sleep(1000) onView(withId(R.id.list)) .perform(actionOnItemAtPosition (1, clickChildViewWithId(R.id.sensitiveWarning))) - Thread.sleep(1000) onView(withId(R.id.list)) .check(matches(atPosition(1, not(withId(R.id.sensitiveWarning))))) @@ -232,45 +233,19 @@ class HomeFeedTest { @Test fun performClickOnSensitiveWarningTabs() { + waitForView(R.id.username) + onView(withId(R.id.list)).perform(scrollToPosition(0)) - Thread.sleep(1000) onView(first(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - Thread.sleep(1000) onView(withId(R.id.list)) .perform(actionOnItemAtPosition (0, clickChildViewWithId(R.id.sensitiveWarning))) - Thread.sleep(1000) onView(first(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.GONE))) } - @Test - fun doubleTapLikerWorks() { - //Get initial like count - val likes = getText(first(withId(R.id.nlikes))) - val nlikes = likes!!.split(" ")[0].toInt() - - //Remove sensitive media warning - onView(withId(R.id.list)) - .perform(actionOnItemAtPosition - (0, clickChildViewWithId(R.id.sensitiveWarning))) - Thread.sleep(100) - - //Like the post - onView(withId(R.id.list)) - .perform(actionOnItemAtPosition - (0, clickChildViewWithId(R.id.postPicture))) - onView(withId(R.id.list)) - .perform(actionOnItemAtPosition - (0, clickChildViewWithId(R.id.postPicture))) - //... - Thread.sleep(100) - - //Profit - onView(first(withId(R.id.nlikes))).check(matches((withText("${nlikes + 1} Likes")))) - } /* @Test fun goOfflineShowsPosts() { diff --git a/app/src/androidTest/java/com/h/pixeldroid/IntentTest.kt b/app/src/androidTest/java/com/h/pixeldroid/IntentTest.kt index fc1a9226..2ba72bcd 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/IntentTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/IntentTest.kt @@ -30,9 +30,7 @@ import com.h.pixeldroid.posts.StatusViewHolder import com.h.pixeldroid.utils.api.objects.Account import com.h.pixeldroid.utils.api.objects.Account.Companion.ACCOUNT_TAG import com.h.pixeldroid.settings.AboutActivity -import com.h.pixeldroid.testUtility.MockServer -import com.h.pixeldroid.testUtility.clearData -import com.h.pixeldroid.testUtility.initDB +import com.h.pixeldroid.testUtility.* import org.hamcrest.CoreMatchers import org.hamcrest.Matcher import org.hamcrest.Matchers @@ -47,7 +45,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class IntentTest { - private lateinit var mockServer: MockServer private lateinit var db: AppDatabase private lateinit var context: Context @@ -62,34 +59,14 @@ class IntentTest { @Before fun before() { - mockServer = MockServer() - mockServer.start() - val baseUrl = mockServer.getUrl() - context = ApplicationProvider.getApplicationContext() db = initDB(context) db.clearAllTables() db.instanceDao().insertInstance( - InstanceDatabaseEntity( - uri = baseUrl.toString(), - title = "PixelTest" - ) + testiTestoInstance ) - db.userDao().insertUser( - UserDatabaseEntity( - user_id = "123", - instance_uri = baseUrl.toString(), - username = "Testi", - display_name = "Testi Testo", - avatar_static = "some_avatar_url", - isActive = true, - accessToken = "token", - refreshToken = refreshToken, - clientId = clientId, - clientSecret = clientSecret - ) - ) + db.userDao().insertUser(testiTesto) db.close() Intents.init() @@ -100,81 +77,32 @@ class IntentTest { fun clickingMentionOpensProfile() { ActivityScenario.launch(MainActivity::class.java) - val account = Account("1450", "deerbard_photo", "deerbard_photo", - "https://pixelfed.social/deerbard_photo", "deerbard photography", + val account = Account("265626292148375552", "user2", "user2", + "https://testing2.pixeldroid.org/user2", "User 2", "", - "https://pixelfed.social/storage/avatars/000/000/001/450/SMSep5NoabDam1W8UDMh_avatar.png?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a", - "https://pixelfed.social/storage/avatars/000/000/001/450/SMSep5NoabDam1W8UDMh_avatar.png?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a", + "https://testing2.pixeldroid.org/storage/avatars/default.jpg?v=0", + "https://testing2.pixeldroid.org/storage/avatars/default.jpg?v=0", "", "", false, emptyList(), null, - "2018-08-01T12:58:21.000000Z", 72, 68, 27, + "2021-02-11T23:44:03.000000Z", 0, 1, 1, null, null, false, null) val expectedIntent: Matcher = CoreMatchers.allOf( IntentMatchers.hasExtra(ACCOUNT_TAG, account) ) - Thread.sleep(1000) + waitForView(R.id.description) //Click the mention Espresso.onView(ViewMatchers.withId(R.id.list)) .perform(RecyclerViewActions.actionOnItemAtPosition - (0, clickClickableSpanInDescription("@Dobios"))) + (0, clickClickableSpanInDescription("@user2"))) //Wait a bit Thread.sleep(1000) - //Check that the Profile is shown + //Check that the right intent was launched intended(expectedIntent) } - private fun clickClickableSpanInDescription(textToClick: CharSequence): ViewAction { - return object : ViewAction { - - override fun getConstraints(): Matcher { - return Matchers.instanceOf(TextView::class.java) - } - - override fun getDescription(): String { - return "clicking on a ClickableSpan" - } - - override fun perform(uiController: UiController, view: View) { - val textView = view.findViewById(R.id.description) as TextView - val spannableString = textView.text as SpannableString - - if (spannableString.isEmpty()) { - // TextView is empty, nothing to do - throw NoMatchingViewException.Builder() - .includeViewHierarchy(true) - .withRootView(textView) - .build() - } - - // Get the links inside the TextView and check if we find textToClick - val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java) - if (spans.isNotEmpty()) { - var spanCandidate: ClickableSpan - for (span: ClickableSpan in spans) { - spanCandidate = span - val start = spannableString.getSpanStart(spanCandidate) - val end = spannableString.getSpanEnd(spanCandidate) - val sequence = spannableString.subSequence(start, end) - if (textToClick.toString() == sequence.toString()) { - span.onClick(textView) - return - } - } - } - - // textToClick not found in TextView - throw NoMatchingViewException.Builder() - .includeViewHierarchy(true) - .withRootView(textView) - .build() - - } - } - } - @Test fun clickEditProfileMakesIntent() { @@ -202,6 +130,5 @@ class IntentTest { fun after() { Intents.release() clearData() - mockServer.stop() } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/LoginActivityOfflineTest.kt b/app/src/androidTest/java/com/h/pixeldroid/LoginActivityOfflineTest.kt index 2e976997..d1ca17c3 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/LoginActivityOfflineTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/LoginActivityOfflineTest.kt @@ -1,6 +1,27 @@ package com.h.pixeldroid - /* +import android.content.Context +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector +import com.h.pixeldroid.testUtility.clearData +import com.h.pixeldroid.testUtility.initDB +import com.h.pixeldroid.utils.db.AppDatabase +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.Timeout +import org.junit.runner.RunWith + @RunWith(AndroidJUnit4::class) class LoginActivityOfflineTest { @@ -10,7 +31,7 @@ class LoginActivityOfflineTest { fun switchAirplaneMode() { val device = UiDevice.getInstance(getInstrumentation()) device.openQuickSettings() - device.findObject(UiSelector().textContains("airplane")).click() + device.findObject(UiSelector().textContains("Airplane")).click() device.pressHome() } } @@ -46,6 +67,4 @@ class LoginActivityOfflineTest { db.close() clearData() } -} - - */ \ No newline at end of file +}*/ \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/LoginActivityOnlineTest.kt b/app/src/androidTest/java/com/h/pixeldroid/LoginActivityOnlineTest.kt index 35ece520..dc0a0c73 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/LoginActivityOnlineTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/LoginActivityOnlineTest.kt @@ -19,9 +19,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.h.pixeldroid.utils.db.AppDatabase import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity -import com.h.pixeldroid.testUtility.MockServer import com.h.pixeldroid.testUtility.clearData import com.h.pixeldroid.testUtility.initDB +import com.h.pixeldroid.testUtility.testiTesto +import com.h.pixeldroid.testUtility.testiTestoInstance import org.junit.After import org.junit.Before import org.junit.Rule @@ -35,7 +36,6 @@ class LoginActivityOnlineTest { private lateinit var db: AppDatabase private lateinit var context: Context private lateinit var pref: SharedPreferences - private lateinit var server: MockServer @get:Rule var globalTimeout: Timeout = Timeout.seconds(100) @@ -43,8 +43,6 @@ class LoginActivityOnlineTest { @Before fun setup() { context = ApplicationProvider.getApplicationContext() - server = MockServer() - server.start() context = ApplicationProvider.getApplicationContext() pref = context.getSharedPreferences("com.h.pixeldroid.pref", Context.MODE_PRIVATE) pref.edit().clear().apply() @@ -55,7 +53,6 @@ class LoginActivityOnlineTest { @After fun after() { clearData() - server.stop() } @Test @@ -102,6 +99,7 @@ class LoginActivityOnlineTest { )) } + /* @Test fun correctIntentReturnLoadsMainActivity() { context = ApplicationProvider.getApplicationContext() @@ -109,36 +107,21 @@ class LoginActivityOnlineTest { db.clearAllTables() db.instanceDao().insertInstance( - InstanceDatabaseEntity( - uri = server.getUrl().toString(), - title = "PixelTest" - ) + testiTestoInstance ) - db.userDao().insertUser( - UserDatabaseEntity( - user_id = "123", - instance_uri = server.getUrl().toString(), - username = "Testi", - display_name = "Testi Testo", - avatar_static = "some_avatar_url", - isActive = true, - accessToken = "token", - refreshToken = refreshToken, - clientId = clientId, - clientSecret = clientSecret - ) - ) + db.userDao().insertUser(testiTesto) db.close() pref.edit() - .putString("domain", server.getUrl().toString()) - .putString("clientID", "test_id") - .putString("clientSecret", "test_secret") + .putString("domain", testiTestoInstance.uri) + .putString("clientID", testiTesto.clientId) + .putString("clientSecret", testiTesto.clientSecret) .apply() - val uri = Uri.parse("oauth2redirect://com.h.pixeldroid?code=test_code") + val uri = Uri.parse("oauth2redirect://com.h.pixeldroid?code=$testiTesto.") val intent = Intent(ACTION_VIEW, uri, context, LoginActivity::class.java) ActivityScenario.launch(intent) Thread.sleep(1000) onView(withId(R.id.main_activity_main_linear_layout)).check(matches(isDisplayed())) } + */ } \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/LoginCheckIntent.kt b/app/src/androidTest/java/com/h/pixeldroid/LoginCheckIntent.kt index 37c292ea..43a183bf 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/LoginCheckIntent.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/LoginCheckIntent.kt @@ -1,6 +1,5 @@ package com.h.pixeldroid -import android.content.Context import android.content.Intent import android.content.Intent.ACTION_VIEW import androidx.test.core.app.ActivityScenario @@ -8,14 +7,20 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.scrollTo +import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction import androidx.test.espresso.intent.matcher.IntentMatchers.hasDataString -import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.rule.ActivityTestRule +import androidx.test.uiautomator.UiDevice +import com.h.pixeldroid.BuildConfig.INSTANCE_URI import com.h.pixeldroid.testUtility.clearData +import com.h.pixeldroid.testUtility.waitForView import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString import org.hamcrest.Matcher @@ -28,17 +33,16 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LoginCheckIntent { - private lateinit var context: Context - @get:Rule var globalTimeout: Timeout = Timeout.seconds(100) - @get:Rule - val intentsTestRule = ActivityTestRule(LoginActivity::class.java) + + private lateinit var activityScenario: ActivityScenario @Before fun before() { Intents.init() + activityScenario = ActivityScenario.launch(LoginActivity::class.java) } @Test @@ -46,11 +50,16 @@ class LoginCheckIntent { ActivityScenario.launch(LoginActivity::class.java) val expectedIntent: Matcher = allOf( hasAction(ACTION_VIEW), - hasDataString(containsString("pixelfed.de")) + hasDataString(containsString(INSTANCE_URI)) ) - Thread.sleep(1000) - onView(withId(R.id.editText)).perform(scrollTo()).perform(ViewActions.replaceText("pixelfed.de"), ViewActions.closeSoftKeyboard()) + waitForView(R.id.editText) + + onView(withId(R.id.editText)).perform(scrollTo()).perform( + ViewActions.replaceText( + INSTANCE_URI + ), ViewActions.closeSoftKeyboard() + ) onView(withId(R.id.connect_instance_button)).perform(scrollTo()).perform(click()) Thread.sleep(3000) @@ -61,22 +70,20 @@ class LoginCheckIntent { @Test fun launchesInstanceInfo() { ActivityScenario.launch(LoginActivity::class.java) - val expectedIntent: Matcher = allOf( - hasAction(ACTION_VIEW), - hasDataString(containsString("pixelfed.org/join")) - ) onView(withId(R.id.whatsAnInstanceTextView)).perform(scrollTo()).perform(click()) - Thread.sleep(3000) - - intended(expectedIntent) + waitForView(R.id.whats_an_instance_explanation) + onView(withText(R.string.whats_an_instance_explanation)) + .inRoot(isDialog()) + .check(matches(isDisplayed())); } @After fun after() { Intents.release() clearData() + activityScenario.close() } } diff --git a/app/src/androidTest/java/com/h/pixeldroid/MockedServerTest.kt b/app/src/androidTest/java/com/h/pixeldroid/MockedServerTest.kt index 789b44a3..68f548da 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/MockedServerTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/MockedServerTest.kt @@ -1,49 +1,44 @@ package com.h.pixeldroid -/* +import android.content.Context +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.android.material.tabs.TabLayout +import com.h.pixeldroid.testUtility.* +import com.h.pixeldroid.utils.db.AppDatabase +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class MockedServerTest { - private lateinit var mockServer: MockServer private lateinit var activityScenario: ActivityScenario private lateinit var db: AppDatabase private lateinit var context: Context @Before fun before(){ - mockServer = MockServer() - mockServer.start() - val baseUrl = mockServer.getUrl() context = ApplicationProvider.getApplicationContext() db = initDB(context) db.clearAllTables() - db.instanceDao().insertInstance( - InstanceDatabaseEntity( - uri = baseUrl.toString(), - title = "PixelTest" - ) - ) + db.instanceDao().insertInstance(testiTestoInstance) - db.userDao().insertUser( - UserDatabaseEntity( - user_id = "123", - instance_uri = baseUrl.toString(), - username = "Testi", - display_name = "Testi Testo", - avatar_static = "some_avatar_url", - isActive = true, - accessToken = "token" - ) - ) + db.userDao().insertUser(testiTesto) db.close() activityScenario = ActivityScenario.launch(MainActivity::class.java) } @After fun after() { clearData() - mockServer.stop() } /* @Test @@ -59,90 +54,53 @@ class MockedServerTest { Thread.sleep(3000) onView(first(withId(R.id.username))).check(matches(withText("memo"))) } - +*/ @Test fun searchHashtags() { activityScenario.onActivity{ a -> a.findViewById(R.id.tabs).getTabAt(1)?.select() } - Thread.sleep(1000) - onView(withId(R.id.searchEditText)).perform(ViewActions.replaceText("#caturday"), ViewActions.closeSoftKeyboard()) + onView(withId(R.id.search)).perform(typeSearchViewText("#randomNoise")) - onView(withId(R.id.searchButton)).perform(click()) - Thread.sleep(3000) - onView(first(withId(R.id.tag_name))).check(matches(withText("#caturday"))) + waitForView(R.id.tag_name) + + onView(first(withId(R.id.tag_name))).check(matches(withText("#randomNoise"))) } - */ @Test fun openDiscoverPost(){ activityScenario.onActivity{ a -> a.findViewById(R.id.tabs).getTabAt(1)?.select() } - Thread.sleep(1000) - onView(first(withId(R.id.postPreview))).perform(click()) - Thread.sleep(1000) - onView(withId(R.id.username)).check(matches(withText("Arthur"))) + waitForView(R.id.postPreview) + + onView(first(withId(R.id.postPreview))).perform(click()) + + waitForView(R.id.username) + + onView(withId(R.id.username)).check(matches(withSubstring("User "))) } -/* + @Test fun searchAccounts() { activityScenario.onActivity{ a -> a.findViewById(R.id.tabs).getTabAt(1)?.select() } - Thread.sleep(1000) - onView(withId(R.id.searchEditText)).perform(ViewActions.replaceText("@dansup"), ViewActions.closeSoftKeyboard()) + waitForView(R.id.search) - onView(withId(R.id.searchButton)).perform(click()) - Thread.sleep(3000) - onView(first(withId(R.id.account_entry_username))).check(matches(withText("dansup"))) + onView(withId(R.id.search)).perform(typeSearchViewText("@user3")) + + waitForView(R.id.account_entry_username) + + onView(first(withId(R.id.account_entry_username))).check(matches(withText("User 3"))) } - */ - - @Test - fun clickFollowButton() { - //Get initial like count - onView(withId(R.id.list)) - .perform(actionOnItemAtPosition - (0, clickChildViewWithId(R.id.username))) - - Thread.sleep(1000) - - // Unfollow - onView(withId(R.id.followButton)).perform((click())) - Thread.sleep(1000) - onView(withId(R.id.followButton)).check(matches(withText("Follow"))) - - // Follow - onView(withId(R.id.followButton)).perform((click())) - Thread.sleep(1000) - onView(withId(R.id.followButton)).check(matches(withText("Unfollow"))) - } - - @Test - fun clickOtherUserFollowers() { - //Get initial like count - onView(withId(R.id.list)) - .perform(actionOnItemAtPosition - (0, clickChildViewWithId(R.id.username))) - - Thread.sleep(1000) - - // Open followers list - onView(withId(R.id.nbFollowersTextView)).perform((click())) - Thread.sleep(1000) - // Open follower's profile - onView(withText("ete2")).perform((click())) - Thread.sleep(1000) - - onView(withId(R.id.accountNameTextView)).check(matches(withText("Christian"))) - } +/*TODO test notifications (harder since they disappear after 6 months... @Test fun testNotificationsList() { @@ -156,7 +114,7 @@ class MockedServerTest { onView(withId(R.id.view_pager)).perform(ViewActions.swipeDown()) Thread.sleep(1000) - onView(withText("Dobios followed you")).check(matches(withId(R.id.notification_type))) + onView(withText("user2 followed you")).check(matches(withId(R.id.notification_type))) } @Test @@ -222,7 +180,7 @@ class MockedServerTest { Thread.sleep(1000) onView(first(withText("Andrea"))).check(matches(withId(R.id.username))) - } + }*/ @Test fun swipingRightStopsAtHomepage() { @@ -230,7 +188,8 @@ class MockedServerTest { a -> a.findViewById(R.id.tabs).getTabAt(4)?.select() } // go to the last tab - Thread.sleep(1000) + waitForView(R.id.main_activity_main_linear_layout) + onView(withId(R.id.main_activity_main_linear_layout)) .perform(ViewActions.swipeRight()) // notifications .perform(ViewActions.swipeRight()) // camera @@ -246,7 +205,8 @@ class MockedServerTest { a -> a.findViewById(R.id.tabs).getTabAt(0)?.select() } - Thread.sleep(1000) + waitForView(R.id.main_activity_main_linear_layout) + onView(withId(R.id.main_activity_main_linear_layout)) .perform(ViewActions.swipeLeft()) // notifications .perform(ViewActions.swipeLeft()) // camera @@ -262,7 +222,8 @@ class MockedServerTest { a -> a.findViewById(R.id.tabs).getTabAt(4)?.select() } // go to the last tab - Thread.sleep(1000) + waitForView(R.id.view_pager) + onView(withId(R.id.view_pager)) .perform(ViewActions.swipeRight()) // notifications .perform(ViewActions.swipeRight()) // camera @@ -276,7 +237,7 @@ class MockedServerTest { a -> assert(a.findViewById(R.id.tabs).getTabAt(0)?.isSelected ?: false) } } - +/* @Test fun censorMatrices() { val array: FloatArray = floatArrayOf( @@ -287,7 +248,5 @@ class MockedServerTest { assert(censorColorMatrix().equals(ColorMatrix(array))) assert(uncensorColorMatrix().equals(ColorMatrix())) - } -} - -*/ \ No newline at end of file + }*/ +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/PostCreationActivityTest.kt b/app/src/androidTest/java/com/h/pixeldroid/PostCreationActivityTest.kt index e5de4444..c3f33937 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/PostCreationActivityTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/PostCreationActivityTest.kt @@ -1,11 +1,41 @@ package com.h.pixeldroid -/* +import android.Manifest +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.net.Uri +import android.util.Log +import android.view.View.VISIBLE +import androidx.core.net.toUri +import androidx.core.os.bundleOf +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import com.h.pixeldroid.postCreation.PostCreationActivity +import com.h.pixeldroid.postCreation.photoEdit.ThumbnailAdapter +import com.h.pixeldroid.settings.AboutActivity +import com.h.pixeldroid.testUtility.* +import com.h.pixeldroid.utils.db.AppDatabase +import org.hamcrest.CoreMatchers.not +import org.junit.* +import org.junit.rules.Timeout +import org.junit.runner.RunWith +import java.io.File + @RunWith(AndroidJUnit4::class) class PostCreationActivityTest { private var testScenario: ActivityScenario? = null - private lateinit var mockServer: MockServer private lateinit var db: AppDatabase private lateinit var context: Context @@ -26,33 +56,19 @@ class PostCreationActivityTest { @Before fun setup() { context = InstrumentationRegistry.getInstrumentation().targetContext - mockServer = MockServer() - mockServer.start() - val baseUrl = mockServer.getUrl() db = initDB(context) db.clearAllTables() db.instanceDao().insertInstance( - InstanceDatabaseEntity( - uri = baseUrl.toString(), - title = "PixelTest" - ) + testiTestoInstance ) db.userDao().insertUser( - UserDatabaseEntity( - user_id = "123", - instance_uri = baseUrl.toString(), - username = "Testi", - display_name = "Testi Testo", - avatar_static = "some_avatar_url", - isActive = true, - accessToken = "token" - ) + testiTesto ) db.close() - var uri1: String = "" - var uri2: String = "" + var uri1: Uri? = null + var uri2: Uri? = null val scenario = ActivityScenario.launch(AboutActivity::class.java) scenario.onActivity { val image1 = Bitmap.createBitmap(500, 500, Bitmap.Config.ARGB_8888) @@ -63,28 +79,32 @@ class PostCreationActivityTest { val file1 = File.createTempFile("temp_img1", ".png") val file2 = File.createTempFile("temp_img2", ".png") file1.writeBitmap(image1) - uri1 = file1.toUri().toString() + uri1 = file1.toUri() file2.writeBitmap(image2) - uri2 = file2.toUri().toString() - Log.d("test", uri1+"\n"+uri2) + uri2 = file2.toUri() } - val intent = Intent(context, PostCreationActivity::class.java).putExtra("pictures_uri", arrayListOf(uri1, uri2)) + val intent = Intent(context, PostCreationActivity::class.java) + + intent.clipData = ClipData("", emptyArray(), ClipData.Item(uri1)) + intent.clipData!!.addItem(ClipData.Item(uri2)) + testScenario = ActivityScenario.launch(intent) } @After fun after() { clearData() - mockServer.stop() } @Test + @Ignore("Annoying to deal with and also sometimes the intent is not working as it should") fun createPost() { onView(withId(R.id.post_creation_send_button)).perform(click()) // should send on main activity - onView(withId(R.id.main_activity_main_linear_layout)).check(matches(isDisplayed())) + Thread.sleep(3000) + onView(withId(R.id.list)).check(matches(isDisplayed())) } - +/* @Test fun errorShown() { testScenario!!.onActivity { a -> a.upload_error.visibility = VISIBLE } @@ -113,9 +133,9 @@ class PostCreationActivityTest { ) ) Thread.sleep(1000) - onView(withId(R.id.action_upload)).perform(click()) + onView(withId(R.id.action_save)).perform(click()) Thread.sleep(1000) - onView(withId(R.id.image_grid)).check(matches(isDisplayed())) + onView(withId(R.id.carousel)).check(matches(isDisplayed())) } @Test @@ -127,5 +147,5 @@ class PostCreationActivityTest { ) ) Thread.sleep(1000) - } -}*/ \ No newline at end of file + }*/ +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/PostCreationFragmentTest.kt b/app/src/androidTest/java/com/h/pixeldroid/PostCreationFragmentTest.kt index 4dea5921..f5317e69 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/PostCreationFragmentTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/PostCreationFragmentTest.kt @@ -16,13 +16,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import com.google.android.material.tabs.TabLayout +import com.h.pixeldroid.testUtility.* import com.h.pixeldroid.utils.db.AppDatabase -import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity -import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity -import com.h.pixeldroid.testUtility.MockServer -import com.h.pixeldroid.testUtility.clearData -import com.h.pixeldroid.testUtility.initDB -import kotlinx.android.synthetic.main.activity_main.* import org.hamcrest.Matcher import org.junit.After import org.junit.Before @@ -48,12 +44,12 @@ class PostCreationFragmentTest { onView(withId(R.id.drawer_layout)) .perform(swipeLeft()) .perform(swipeLeft()) - Thread.sleep(300) + waitForView(R.id.photo_view_button) } - // upload intent + // image choosing intent @Test - fun uploadButtonLaunchesGalleryIntent() { + fun galleryButtonLaunchesGalleryIntent() { val expectedIntent: Matcher = hasAction(Intent.ACTION_CHOOSER) intending(expectedIntent) onView(withId(R.id.photo_view_button)).perform(click()) @@ -64,7 +60,6 @@ class PostCreationFragmentTest { @RunWith(AndroidJUnit4::class) class PostFragmentUITests { - private lateinit var mockServer: MockServer private lateinit var context: Context @get:Rule @@ -75,48 +70,31 @@ class PostFragmentUITests { @Before fun setup() { context = InstrumentationRegistry.getInstrumentation().targetContext - mockServer = MockServer() - mockServer.start() - val baseUrl = mockServer.getUrl() db = initDB(context) db.clearAllTables() db.instanceDao().insertInstance( - InstanceDatabaseEntity( - uri = baseUrl.toString(), - title = "PixelTest" - ) + testiTestoInstance ) db.userDao().insertUser( - UserDatabaseEntity( - user_id = "123", - instance_uri = baseUrl.toString(), - username = "Testi", - display_name = "Testi Testo", - avatar_static = "some_avatar_url", - isActive = true, - accessToken = "token", - refreshToken = refreshToken, - clientId = clientId, - clientSecret = clientSecret - ) + testiTesto ) db.close() - Thread.sleep(300) } @After fun after() { clearData() - mockServer.stop() } @Test fun newPostUiTest() { ActivityScenario.launch(MainActivity::class.java).onActivity { - a -> a.tabs.getTabAt(2)!!.select() + it.findViewById(R.id.tabs).getTabAt(2)!!.select() } - Thread.sleep(1500) + + waitForView(R.id.photo_view_button) + onView(withId(R.id.photo_view_button)).check(matches(isDisplayed())) onView(withId(R.id.camera_capture_button)).check(matches(isDisplayed())) } diff --git a/app/src/androidTest/java/com/h/pixeldroid/PostTest.kt b/app/src/androidTest/java/com/h/pixeldroid/PostTest.kt index 1086efc5..7426a9c0 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/PostTest.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/PostTest.kt @@ -13,14 +13,16 @@ import androidx.test.espresso.matcher.ViewMatchers.* import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.h.pixeldroid.BuildConfig.INSTANCE_URI import com.h.pixeldroid.posts.PostActivity import com.h.pixeldroid.utils.db.AppDatabase import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import com.h.pixeldroid.utils.api.objects.* -import com.h.pixeldroid.testUtility.MockServer import com.h.pixeldroid.testUtility.clearData import com.h.pixeldroid.testUtility.initDB +import com.h.pixeldroid.testUtility.testiTesto +import com.h.pixeldroid.testUtility.testiTestoInstance import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.Matcher import org.junit.* @@ -34,7 +36,6 @@ class PostTest { private lateinit var context: Context private lateinit var db: AppDatabase - private lateinit var mockServer: MockServer @get:Rule var globalTimeout: Timeout = Timeout.seconds(100) @@ -42,32 +43,13 @@ class PostTest { @Before fun before(){ context = InstrumentationRegistry.getInstrumentation().targetContext - mockServer = MockServer() - mockServer.start() - val baseUrl = mockServer.getUrl() db = initDB(context) db.clearAllTables() db.instanceDao().insertInstance( - InstanceDatabaseEntity( - uri = baseUrl.toString(), - title = "PixelTest" - ) + testiTestoInstance ) - db.userDao().insertUser( - UserDatabaseEntity( - user_id = "123", - instance_uri = baseUrl.toString(), - username = "Testi", - display_name = "Testi Testo", - avatar_static = "some_avatar_url", - isActive = true, - accessToken = "token", - refreshToken = refreshToken, - clientId = clientId, - clientSecret = clientSecret - ) - ) + db.userDao().insertUser(testiTesto) db.close() Intents.init() } @@ -76,14 +58,15 @@ class PostTest { fun saveToGalleryTestSimplePost() { val attachment = Attachment( id = "12", - url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0", + meta = null ) val post = Status( id = "12", account = Account( id = "12", - username = "douze", - url = "https://pixelfed.de/douze" + username = "SQDFSQDF", + url = "$INSTANCE_URI/pixeldroid", ), media_attachments = listOf(attachment) ) @@ -106,18 +89,20 @@ class PostTest { fun saveToGalleryTestAlbum() { val attachment1 = Attachment( id = "12", - url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0", + meta = null ) val attachment2 = Attachment( id = "13", - url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0", + meta = null ) val post = Status( id = "12", account = Account( id = "12", username = "douze", - url = "https://pixelfed.de/douze" + url = "$INSTANCE_URI/pixeldroid", ), media_attachments = listOf(attachment1, attachment2) ) @@ -140,17 +125,18 @@ class PostTest { fun shareTestSimplePost() { val expectedIntent: Matcher = IntentMatchers.hasAction(Intent.ACTION_CHOOSER) val attachment = Attachment( - id = "12", - url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + id = "12", + url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0", + meta = null ) val post = Status( - id = "12", - account = Account( id = "12", - username = "douze", - url = "https://pixelfed.de/douze" - ), - media_attachments = listOf(attachment) + account = Account( + id = "12", + username = "douze", + url = "$INSTANCE_URI/pixeldroid", + ), + media_attachments = listOf(attachment) ) val intent = Intent(context, PostActivity::class.java) intent.putExtra(Status.POST_TAG, post) @@ -166,20 +152,22 @@ class PostTest { val expectedIntent: Matcher = IntentMatchers.hasAction(Intent.ACTION_CHOOSER) val attachment1 = Attachment( id = "12", - url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0", + meta = null ) val attachment2 = Attachment( - id = "13", - url = "https://wiki.gnugen.ch/lib/tpl/gnugen/images/logo_web.png" + id = "13", + url = "$INSTANCE_URI/storage/avatars/default.jpg?v=0", + meta = null ) val post = Status( - id = "12", - account = Account( id = "12", - username = "douze", - url = "https://pixelfed.de/douze" - ), - media_attachments = listOf(attachment1, attachment2) + account = Account( + id = "12", + username = "douze", + url = "$INSTANCE_URI/pixeldroid", + ), + media_attachments = listOf(attachment1, attachment2) ) val intent = Intent(context, PostActivity::class.java) intent.putExtra(Status.POST_TAG, post) @@ -208,7 +196,7 @@ class PostTest { media_attachments= listOf( Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg", preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg", - remote_url=null, text_url=null, description=null, blurhash=null) + remote_url=null, text_url=null, description=null, blurhash=null, meta = null) ), application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(), tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)), @@ -235,7 +223,7 @@ class PostTest { media_attachments= listOf( Attachment(id="15888", type= Attachment.AttachmentType.image, url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB.jpeg", preview_url="https://pixelfed.de/storage/m/113a3e2124a33b1f5511e531953f5ee48456e0c7/34dd6d9fb1762dac8c7ddeeaf789d2d8fa083c9f/JtjO0eAbELpgO1UZqF5ydrKbCKRVyJUM1WAaqIeB_thumb.jpeg", - remote_url=null, text_url=null, description=null, blurhash=null) + remote_url=null, text_url=null, description=null, blurhash=null, meta = null) ), application= Application(name="web", website=null, vapid_key=null), mentions=emptyList(), tags= listOf(Tag(name="hiking", url="https://pixelfed.de/discover/tags/hiking", history=null), Tag(name="nature", url="https://pixelfed.de/discover/tags/nature", history=null), Tag(name="rotavicentina", url="https://pixelfed.de/discover/tags/rotavicentina", history=null)), @@ -243,14 +231,13 @@ class PostTest { in_reply_to_id=null, in_reply_to_account=null, reblog=null, poll=null, card=null, language=null, text=null, favourited=false, reblogged=false, muted=false, bookmarked=false, pinned=false) Assert.assertEquals("${status.reblogs_count} Shares", - status.getNShares(getInstrumentation().targetContext)) + status.getNShares(context)) } @After fun after() { Intents.release() clearData() - mockServer.stop() } } diff --git a/app/src/androidTest/java/com/h/pixeldroid/ProfileTest.kt b/app/src/androidTest/java/com/h/pixeldroid/ProfileTest.kt new file mode 100644 index 00000000..a1acbf45 --- /dev/null +++ b/app/src/androidTest/java/com/h/pixeldroid/ProfileTest.kt @@ -0,0 +1,102 @@ +package com.h.pixeldroid + +import android.content.Context +import android.content.Intent +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.h.pixeldroid.postCreation.PostCreationActivity +import com.h.pixeldroid.profile.ProfileActivity +import com.h.pixeldroid.testUtility.* +import com.h.pixeldroid.utils.api.objects.Account +import com.h.pixeldroid.utils.db.AppDatabase +import okhttp3.internal.wait +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.Serializable + +@RunWith(AndroidJUnit4::class) +class ProfileTest { + private lateinit var activityScenario: ActivityScenario + private lateinit var db: AppDatabase + private lateinit var context: Context + + @Before + fun before(){ + context = ApplicationProvider.getApplicationContext() + db = initDB(context) + db.clearAllTables() + db.instanceDao().insertInstance(testiTestoInstance) + + db.userDao().insertUser(testiTesto) + db.close() + + val intent = Intent(context, ProfileActivity::class.java) + val account = Account(id = "265472486651596800", username = "pixeldroid", acct = "pixeldroid", url = "https://testing2.pixeldroid.org/pixeldroid", display_name = "PixelDroid Developer", avatar = "https://testing2.pixeldroid.org/storage/avatars/default.jpg?v=0", avatar_static = "https://testing2.pixeldroid.org/storage/avatars/default.jpg?v=0", locked = false, emojis = arrayListOf(), discoverable = null, created_at = "2021-02-11T13:32:53.000000Z", statuses_count = 1, followers_count = 1, following_count = 1, moved = null, fields = null, bot = false, source = null) + intent.putExtra(Account.ACCOUNT_TAG, account) + activityScenario = ActivityScenario.launch(intent) + onView(withId(R.id.profileRefreshLayout)).perform(swipeDown()) + Thread.sleep(2000) + } + @After + fun after() { + clearData() + } + + + @Test + fun clickFollowButton() { + if (onView(ViewMatchers.withText("Unfollow")).isDisplayed()) { + //Currently following + + // Unfollow + follow("Follow") + + // Follow + follow("Unfollow") + } else { + //Currently not following + + // Follow + follow("Unfollow") + + // Unfollow + follow("Follow") + } + } + + private fun follow(follow_or_unfollow: String){ + onView(withId(R.id.followButton)).perform((ViewActions.click())) + Thread.sleep(1000) + onView(withId(R.id.followButton)).check(ViewAssertions.matches(ViewMatchers.withText(follow_or_unfollow))) + } + + + + @Test + fun clickOtherUserFollowers() { + // Open followers list + onView(withId(R.id.nbFollowersTextView)).perform((ViewActions.click())) + + waitForView(R.id.account_entry_username) + + // Open follower's profile + onView(ViewMatchers.withText("testi testo")).perform((ViewActions.click())) + + waitForView(R.id.editButton) + + //Check that our own profile opened + onView(withId(R.id.editButton)).check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/testUtility/CustomMatchers.kt b/app/src/androidTest/java/com/h/pixeldroid/testUtility/CustomMatchers.kt index a4aa3896..de1ce3f2 100644 --- a/app/src/androidTest/java/com/h/pixeldroid/testUtility/CustomMatchers.kt +++ b/app/src/androidTest/java/com/h/pixeldroid/testUtility/CustomMatchers.kt @@ -1,153 +1,284 @@ package com.h.pixeldroid.testUtility +import android.text.SpannableString +import android.text.style.ClickableSpan import android.view.View import android.widget.EditText import android.widget.TextView +import androidx.appcompat.widget.SearchView import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.Espresso -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction +import androidx.test.espresso.* import androidx.test.espresso.action.* +import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.BoundedMatcher import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.espresso.util.HumanReadables +import androidx.test.espresso.util.TreeIterables +import com.h.pixeldroid.R import org.hamcrest.BaseMatcher +import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Description import org.hamcrest.Matcher +import org.hamcrest.Matchers +import java.util.concurrent.TimeoutException -abstract class CustomMatchers { - companion object { - fun first(matcher: Matcher): Matcher? { - return object : BaseMatcher() { - var isFirst = true - override fun describeTo(description: org.hamcrest.Description?) { - description?.appendText("first matching item") - } +fun ViewInteraction.isDisplayed(): Boolean { + return try { + check(matches(ViewMatchers.isDisplayed())) + true + } catch (e: NoMatchingViewException) { + false + } +} - override fun matches(item: Any?): Boolean { - if (isFirst && matcher.matches(item)) { - isFirst = false - return true +/** + * Waits for a view to appear in the hierarchy + * Doesn't work if the root changes (since it operates on the root!) + * @param viewId The id of the view to wait for. + */ +fun waitForView(viewId: Int) { + Espresso.onView(isRoot()).perform(waitForViewViewAction(viewId)) +} + +/** + * This ViewAction tells espresso to wait till a certain view is found in the view hierarchy. + * @param viewId The id of the view to wait for. + */ +private fun waitForViewViewAction(viewId: Int): ViewAction { + // The maximum time which espresso will wait for the view to show up (in milliseconds) + val timeOut = 5000 + return object : ViewAction { + override fun getConstraints(): Matcher { + return isRoot() + } + + override fun getDescription(): String { + return "wait for a specific view with id $viewId; during $timeOut millis." + } + + override fun perform(uiController: UiController, rootView: View) { + uiController.loopMainThreadUntilIdle() + val startTime = System.currentTimeMillis() + val endTime = startTime + timeOut + val viewMatcher = withId(viewId) + + do { + // Iterate through all views on the screen and see if the view we are looking for is there already + for (child in TreeIterables.breadthFirstViewTraversal(rootView)) { + // found view with required ID + if (viewMatcher.matches(child)) { + return } + } + // Loops the main thread for a specified period of time. + // Control may not return immediately, instead it'll return after the provided delay has passed and the queue is in an idle state again. + uiController.loopMainThreadForAtLeast(100) + } while (System.currentTimeMillis() < endTime) // in case of a timeout we throw an exception -> test fails + throw PerformException.Builder() + .withCause(TimeoutException()) + .withActionDescription(this.description) + .withViewDescription(HumanReadables.describe(rootView)) + .build() + } + } +} + +fun clickClickableSpanInDescription(textToClick: CharSequence): ViewAction { + return object : ViewAction { + + override fun getConstraints(): Matcher { + return Matchers.instanceOf(TextView::class.java) + } + + override fun getDescription(): String { + return "clicking on a ClickableSpan" + } + + override fun perform(uiController: UiController, view: View) { + val textView = view.findViewById(R.id.description) as TextView + val spannableString = SpannableString(textView.text) + + if (spannableString.isEmpty()) { + // TextView is empty, nothing to do + throw NoMatchingViewException.Builder() + .includeViewHierarchy(true) + .withRootView(textView) + .build() + } + + // Get the links inside the TextView and check if we find textToClick + val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java) + if (spans.isNotEmpty()) { + var spanCandidate: ClickableSpan + for (span: ClickableSpan in spans) { + spanCandidate = span + val start = spannableString.getSpanStart(spanCandidate) + val end = spannableString.getSpanEnd(spanCandidate) + val sequence = spannableString.subSequence(start, end) + if (textToClick.toString() == sequence.toString()) { + span.onClick(textView) + return + } + } + } + + // textToClick not found in TextView + throw NoMatchingViewException.Builder() + .includeViewHierarchy(true) + .withRootView(textView) + .build() + + } + } +} + +fun typeSearchViewText(text: String): ViewAction { + return object : ViewAction { + override fun getDescription(): String { + return "Change view text" + } + + override fun getConstraints(): Matcher { + return allOf(isDisplayed(), isAssignableFrom(SearchView::class.java)) + } + + override fun perform(uiController: UiController?, view: View?) { + (view as SearchView).setQuery(text, true) + } + } +} + +fun first(matcher: Matcher): Matcher { + return object : BaseMatcher() { + var isFirst = true + override fun describeTo(description: org.hamcrest.Description?) { + description?.appendText("first matching item") + } + + override fun matches(item: Any?): Boolean { + if (isFirst && matcher.matches(item)) { + isFirst = false + return true + } + return false + } + + } +} + +fun second(matcher: Matcher): Matcher { + return object : BaseMatcher() { + var isFirst = true + override fun describeTo(description: org.hamcrest.Description?) { + description?.appendText("second matching item") + } + + override fun matches(item: Any?): Boolean { + if (isFirst && matcher.matches(item)) { + isFirst = false + return false + } else if (!isFirst && matcher.matches(item)) + return true + return false + } + + } +} + + +fun atPosition(position: Int, itemMatcher: Matcher): Matcher { + return object : BoundedMatcher(RecyclerView::class.java) { + override fun describeTo(description: Description) { + description.appendText("has item at position $position: ") + itemMatcher.describeTo(description) + } + + override fun matchesSafely(view: RecyclerView): Boolean { + val viewHolder = view.findViewHolderForAdapterPosition(position) + ?: // has no item on such position return false - } - - } - } - - fun second(matcher: Matcher): Matcher? { - return object : BaseMatcher() { - var isFirst = true - override fun describeTo(description: org.hamcrest.Description?) { - description?.appendText("second matching item") - } - - override fun matches(item: Any?): Boolean { - if (isFirst && matcher.matches(item)) { - isFirst = false - return false - } else if (!isFirst && matcher.matches(item)) - return true - return false - } - - } + return itemMatcher.matches(viewHolder.itemView) } + } +} - fun atPosition(position: Int, itemMatcher: Matcher): Matcher? { - return object : BoundedMatcher(RecyclerView::class.java) { - override fun describeTo(description: Description) { - description.appendText("has item at position $position: ") - itemMatcher.describeTo(description) - } - - override fun matchesSafely(view: RecyclerView): Boolean { - val viewHolder = view.findViewHolderForAdapterPosition(position) - ?: // has no item on such position - return false - return itemMatcher.matches(viewHolder.itemView) - } - } - } - - - /** - * @param percent can be 1 or 0 - * 1: swipes all the way up - * 0: swipes half way up - */ - fun slowSwipeUp(percent: Boolean) : ViewAction { - return ViewActions.actionWithAssertions( - GeneralSwipeAction( +/** + * @param percent can be 1 or 0 + * 1: swipes all the way up + * 0: swipes half way up + */ +fun slowSwipeUp(percent: Boolean): ViewAction { + return ViewActions.actionWithAssertions( + GeneralSwipeAction( Swipe.SLOW, GeneralLocation.BOTTOM_CENTER, if (percent) GeneralLocation.TOP_CENTER else GeneralLocation.CENTER, Press.FINGER - ) ) - } + ) +} - /** - * @param percent can be 1 or 0 - * 1: swipes all the way left - * 0: swipes half way left - */ - fun slowSwipeLeft(percent: Boolean) : ViewAction { - return ViewActions.actionWithAssertions( - GeneralSwipeAction( +/** + * @param percent can be 1 or 0 + * 1: swipes all the way left + * 0: swipes half way left + */ +fun slowSwipeLeft(percent: Boolean): ViewAction { + return ViewActions.actionWithAssertions( + GeneralSwipeAction( Swipe.SLOW, GeneralLocation.CENTER_RIGHT, if (percent) GeneralLocation.CENTER_LEFT else GeneralLocation.CENTER, Press.FINGER - ) ) + ) +} + +fun getText(matcher: Matcher?): String? { + val stringHolder = arrayOf(null) + Espresso.onView(matcher).perform(object : ViewAction { + override fun getConstraints(): Matcher { + return ViewMatchers.isAssignableFrom(TextView::class.java) } - fun getText(matcher: Matcher?): String? { - val stringHolder = arrayOf(null) - Espresso.onView(matcher).perform(object : ViewAction { - override fun getConstraints(): Matcher { - return ViewMatchers.isAssignableFrom(TextView::class.java) - } - - override fun getDescription(): String { - return "getting text from a TextView" - } - - override fun perform( - uiController: UiController, - view: View - ) { - val tv = view as TextView //Save, because of check in getConstraints() - stringHolder[0] = tv.text.toString() - } - }) - return stringHolder[0] + override fun getDescription(): String { + return "getting text from a TextView" } - fun clickChildViewWithId(id: Int) = object : ViewAction { - - override fun getConstraints() = null - - override fun getDescription() = "click child view with id $id" - - override fun perform(uiController: UiController, view: View) { - val v = view.findViewById(id) - v.performClick() - } + override fun perform( + uiController: UiController, + view: View, + ) { + val tv = view as TextView //Save, because of check in getConstraints() + stringHolder[0] = tv.text.toString() } + }) + return stringHolder[0] +} - fun typeTextInViewWithId(id: Int, text: String) = object : ViewAction { +fun clickChildViewWithId(id: Int) = object : ViewAction { - override fun getConstraints() = null + override fun getConstraints() = null - override fun getDescription() = "click child view with id $id" + override fun getDescription() = "click child view with id $id" - override fun perform(uiController: UiController, view: View) { - val v = view.findViewById(id) - v.text.append(text) - } - } + override fun perform(uiController: UiController, view: View) { + val v = view.findViewById(id) + v.performClick() + } +} + +fun typeTextInViewWithId(id: Int, text: String) = object : ViewAction { + + override fun getConstraints() = null + + override fun getDescription() = "click child view with id $id" + + override fun perform(uiController: UiController, view: View) { + val v = view.findViewById(id) + v.text.append(text) } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/testUtility/JsonValues.kt b/app/src/androidTest/java/com/h/pixeldroid/testUtility/JsonValues.kt deleted file mode 100644 index ecfb5368..00000000 --- a/app/src/androidTest/java/com/h/pixeldroid/testUtility/JsonValues.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.h.pixeldroid.testUtility - -import com.google.gson.Gson -import com.h.pixeldroid.utils.api.objects.Application - -class JsonValues { - companion object { - const val likedJson = """{"id":"156491373246287872","created_at":"2020-04-16T20:00:50.000000Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https:\/\/pixelfed.de\/p\/machintuck\/156491373246287872","url":"https:\/\/pixelfed.de\/p\/machintuck\/156491373246287872","replies_count":1,"reblogs_count":13,"favourites_count":3,"reblogged":false,"favourited":true,"muted":false,"bookmarked":false,"pinned":false,"content":"@Dobios<\/a> @Dante<\/a>","reblog":null,"application":{"name":"web","website":null},"mentions":[{"id":"136800034732773376","url":"https:\/\/pixelfed.de\/Dobios","username":"Dobios","acct":"Dobios"},{"id":"136453537340198912","url":"https:\/\/pixelfed.de\/dante","username":"dante","acct":"dante"}],"tags":[{"name":"mushroom","url":"https:\/\/pixelfed.de\/discover\/tags\/mushroom"},{"name":"commentsstillbroken","url":"https:\/\/pixelfed.de\/discover\/tags\/commentsstillbroken"},{"name":"fixyourapi","url":"https:\/\/pixelfed.de\/discover\/tags\/fixyourapi"},{"name":"pls","url":"https:\/\/pixelfed.de\/discover\/tags\/pls"}],"emojis":[],"card":null,"poll":null,"account":{"id":"145183325781364736","username":"machintuck","acct":"machintuck","display_name":"Arthur","locked":false,"created_at":"2020-03-16T15:06:42.000000Z","followers_count":4,"following_count":4,"statuses_count":5,"note":"","url":"https:\/\/pixelfed.de\/machintuck","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"19228","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP_thumb.jpeg","text_url":null,"meta":null,"description":null}]}""" - const val unlikeJson = """{"id":"156491373246287872","created_at":"2020-04-16T20:00:50.000000Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","language":"en","uri":"https:\/\/pixelfed.de\/p\/machintuck\/156491373246287872","url":"https:\/\/pixelfed.de\/p\/machintuck\/156491373246287872","replies_count":1,"reblogs_count":13,"favourites_count":2,"reblogged":false,"favourited":false,"muted":false,"bookmarked":false,"pinned":false,"content":"@Dobios<\/a> @Dante<\/a>","reblog":null,"application":{"name":"web","website":null},"mentions":[{"id":"136800034732773376","url":"https:\/\/pixelfed.de\/Dobios","username":"Dobios","acct":"Dobios"},{"id":"136453537340198912","url":"https:\/\/pixelfed.de\/dante","username":"dante","acct":"dante"}],"tags":[{"name":"mushroom","url":"https:\/\/pixelfed.de\/discover\/tags\/mushroom"},{"name":"commentsstillbroken","url":"https:\/\/pixelfed.de\/discover\/tags\/commentsstillbroken"},{"name":"fixyourapi","url":"https:\/\/pixelfed.de\/discover\/tags\/fixyourapi"},{"name":"pls","url":"https:\/\/pixelfed.de\/discover\/tags\/pls"}],"emojis":[],"card":null,"poll":null,"account":{"id":"145183325781364736","username":"machintuck","acct":"machintuck","display_name":"Arthur","locked":false,"created_at":"2020-03-16T15:06:42.000000Z","followers_count":4,"following_count":4,"statuses_count":5,"note":"","url":"https:\/\/pixelfed.de\/machintuck","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"19228","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP_thumb.jpeg","text_url":null,"meta":null,"description":null}]}""" - const val mediaUploadResponseJson = """ - { - "id": "22348641", - "type": "image", - "url": "https://files.mastodon.social/media_attachments/files/022/348/641/original/cebc6d51be03e509.jpeg", - "preview_url": "https://files.mastodon.social/media_attachments/files/022/348/641/small/cebc6d51be03e509.jpeg", - "remote_url": null, - "text_url": "https://mastodon.social/media/4Zj6ewxzzzDi0g8JnZQ", - "meta": { - "focus": { - "x": -0.69, - "y": 0.42 - }, - "original": { - "width": 640, - "height": 480, - "size": "640x480", - "aspect": 1.3333333333333333 - }, - "small": { - "width": 461, - "height": 346, - "size": "461x346", - "aspect": 1.3323699421965318 - } - }, - "description": "test uploaded via api", - "blurhash": "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}" - } - """ - const val accountJson = "{\n" + - " \"id\": \"1450\",\n" + - " \"username\": \"deerbard_photo\",\n" + - " \"acct\": \"deerbard_photo\",\n" + - " \"display_name\": \"deerbard photography\",\n" + - " \"locked\": false,\n" + - " \"created_at\": \"2018-08-01T12:58:21.000000Z\",\n" + - " \"followers_count\": 68,\n" + - " \"following_count\": 27,\n" + - " \"statuses_count\": 72,\n" + - " \"note\": \"\",\n" + - " \"url\": \"https://pixelfed.social/deerbard_photo\",\n" + - " \"avatar\": \"https://pixelfed.social/storage/avatars/000/000/001/450/SMSep5NoabDam1W8UDMh_avatar.png?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a\",\n" + - " \"avatar_static\": \"https://pixelfed.social/storage/avatars/000/000/001/450/SMSep5NoabDam1W8UDMh_avatar.png?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a\",\n" + - " \"header\": \"\",\n" + - " \"header_static\": \"\",\n" + - " \"emojis\": [],\n" + - " \"moved\": null,\n" + - " \"fields\": null,\n" + - " \"bot\": false,\n" + - " \"software\": \"pixelfed\",\n" + - " \"is_admin\": false\n" + - " }" - - const val feedJson = """[{"id":"156491373246287872","created_at":"2020-04-16T20:00:50.000000Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":true,"spoiler_text":"","visibility":"public","language":"en","uri":"https:\/\/pixelfed.de\/p\/machintuck\/156491373246287872","url":"https:\/\/pixelfed.de\/p\/machintuck\/156491373246287872","replies_count":1,"reblogs_count":13,"favourites_count":2,"reblogged":false,"favourited":false,"muted":false,"bookmarked":false,"pinned":false,"content":"@Dobios<\/a> @Dante<\/a>","reblog":null,"application":{"name":"web","website":null},"mentions":[{"id":"136800034732773376","url":"https:\/\/pixelfed.de\/Dobios","username":"Dobios","acct":"Dobios"},{"id":"136453537340198912","url":"https:\/\/pixelfed.de\/dante","username":"dante","acct":"dante"}],"tags":[{"name":"mushroom","url":"https:\/\/pixelfed.de\/discover\/tags\/mushroom"},{"name":"commentsstillbroken","url":"https:\/\/pixelfed.de\/discover\/tags\/commentsstillbroken"},{"name":"fixyourapi","url":"https:\/\/pixelfed.de\/discover\/tags\/fixyourapi"},{"name":"pls","url":"https:\/\/pixelfed.de\/discover\/tags\/pls"}],"emojis":[],"card":null,"poll":null,"account":{"id":"145183325781364736","username":"machintuck","acct":"machintuck","display_name":"Arthur","locked":false,"created_at":"2020-03-16T15:06:42.000000Z","followers_count":4,"following_count":4,"statuses_count":5,"note":"","url":"https:\/\/pixelfed.de\/machintuck","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"15887","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/a1349f5183c2bac7d52880e8f5188df0f3b2d62a\/mvT3nYV6Wdu42Xh56Ny4VYaWU0OzbnC3wjxiqnKS.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/a1349f5183c2bac7d52880e8f5188df0f3b2d62a\/mvT3nYV6Wdu42Xh56Ny4VYaWU0OzbnC3wjxiqnKS_thumb.jpeg","text_url":null,"meta":null,"description":null}, {"id":"19228","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"140349785193451520","uri":"https:\/\/pixelfed.de\/p\/stephan\/140349785193451520","url":"https:\/\/pixelfed.de\/p\/stephan\/140349785193451520","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"","created_at":"2020-03-03T06:59:56.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":2,"reblogged":null,"favourited":null,"muted":null,"sensitive":true,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"908","username":"stephan","acct":"stephan","display_name":"Stephan","locked":false,"created_at":"2019-03-17T07:46:33.000000Z","followers_count":136,"following_count":25,"statuses_count":136,"note":"Musician, software developer & hobby photographer.","url":"https:\/\/pixelfed.de\/stephan","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/000\/000\/000\/908\/5nQzzsB1mkwKaUqQ9GNN_avatar.png?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/000\/000\/000\/908\/5nQzzsB1mkwKaUqQ9GNN_avatar.png?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"15887","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/a1349f5183c2bac7d52880e8f5188df0f3b2d62a\/mvT3nYV6Wdu42Xh56Ny4VYaWU0OzbnC3wjxiqnKS.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/a1349f5183c2bac7d52880e8f5188df0f3b2d62a\/mvT3nYV6Wdu42Xh56Ny4VYaWU0OzbnC3wjxiqnKS_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"0","uri":"https:\/\/pixelfed.de\/p\/fegrimaldi\/140276879742603264","url":"https:\/\/pixelfed.de\/p\/fegrimaldi\/140276879742603264","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"february 2 is the day to give flowers to Iemanj\u00e1. #salvador<\/a> #bahia<\/a> #brazil<\/a> #iemanja<\/a>","created_at":"2020-03-03T02:10:14.000000Z","emojis":[],"replies_count":1,"reblogs_count":0,"favourites_count":1,"reblogged":null,"favourited":null,"muted":null,"sensitive":true,"spoiler_text":"","visibility":"public","mentions":[],"tags":[{"name":"salvador","url":"https:\/\/pixelfed.de\/discover\/tags\/salvador"},{"name":"bahia","url":"https:\/\/pixelfed.de\/discover\/tags\/bahia"},{"name":"brazil","url":"https:\/\/pixelfed.de\/discover\/tags\/brazil"},{"name":"iemanja","url":"https:\/\/pixelfed.de\/discover\/tags\/iemanja"}],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"137257212828585984","username":"fegrimaldi","acct":"fegrimaldi","display_name":"Fernanda Grimaldi","locked":false,"created_at":"2020-02-23T18:11:09.000000Z","followers_count":2,"following_count":7,"statuses_count":2,"note":"a little piece of Bahia in the fediverse.","url":"https:\/\/pixelfed.de\/fegrimaldi","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/013\/725\/721\/282\/858\/598\/4\/oUPBit0TJso1xNhJfFqg_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/013\/725\/721\/282\/858\/598\/4\/oUPBit0TJso1xNhJfFqg_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"15886","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/feb878b4bd60b85ac840670c6b9c809fd76b628b\/lYMrx0WF8LDqn0vTRgNJaRs7stMKtAXrgzpMrWEr.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/feb878b4bd60b85ac840670c6b9c809fd76b628b\/lYMrx0WF8LDqn0vTRgNJaRs7stMKtAXrgzpMrWEr_thumb.jpeg","text_url":null,"meta":null,"description":null}]}]""" - const val notificationsJson = "[{\"id\":\"45945\",\"type\":\"favourite\",\"created_at\":\"2020-03-15T14:49:20.000000Z\",\"account\":{\"id\":\"136800034732773376\",\"username\":\"Dobios\",\"acct\":\"Dobios\",\"display_name\":\"Andrew Dobis\",\"locked\":false,\"created_at\":\"2020-02-22T11:54:29.000000Z\",\"followers_count\":2,\"following_count\":1,\"statuses_count\":0,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Dobios\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"status\":{\"id\":\"144456497894658048\",\"uri\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"url\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"reblog\":null,\"content\":\"Saturn V launch\",\"created_at\":\"2020-03-14T14:58:32.000000Z\",\"emojis\":[],\"replies_count\":0,\"reblogs_count\":1,\"favourites_count\":6,\"reblogged\":null,\"favourited\":null,\"muted\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"mentions\":[],\"tags\":[],\"card\":null,\"poll\":null,\"application\":{\"name\":\"web\",\"website\":null},\"language\":null,\"pinned\":null,\"account\":{\"id\":\"136453537340198912\",\"username\":\"Dante\",\"acct\":\"dante\",\"display_name\":\"Dante\",\"locked\":false,\"created_at\":\"2020-02-21T12:57:38.000000Z\",\"followers_count\":3,\"following_count\":4,\"statuses_count\":1,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/dante\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"media_attachments\":[{\"id\":\"16583\",\"type\":\"image\",\"url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad.jpeg\",\"remote_url\":null,\"preview_url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad_thumb.jpeg\",\"text_url\":null,\"meta\":null,\"description\":null}]}},{\"id\":\"45944\",\"type\":\"follow\",\"created_at\":\"2020-03-15T14:49:11.000000Z\",\"account\":{\"id\":\"136800034732773376\",\"username\":\"Dobios\",\"acct\":\"Dobios\",\"display_name\":\"Andrew Dobis\",\"locked\":false,\"created_at\":\"2020-02-22T11:54:29.000000Z\",\"followers_count\":2,\"following_count\":1,\"statuses_count\":0,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Dobios\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false}},{\"id\":\"45942\",\"type\":\"reblog\",\"created_at\":\"2020-03-15T14:41:04.000000Z\",\"account\":{\"id\":\"144813993922531328\",\"username\":\"Clement\",\"acct\":\"Clement\",\"display_name\":\"Andrea\",\"locked\":false,\"created_at\":\"2020-03-15T14:39:06.000000Z\",\"followers_count\":0,\"following_count\":2,\"statuses_count\":0,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Clement\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"status\":{\"id\":\"144814478708576256\",\"uri\":\"https:\\/\\/pixelfed.de\\/p\\/Clement\\/144814478708576256\",\"url\":\"https:\\/\\/pixelfed.de\\/p\\/Clement\\/144814478708576256\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":136453537340198912,\"reblog\":null,\"content\":\"\",\"created_at\":\"2020-03-15T14:41:02.000000Z\",\"emojis\":[],\"replies_count\":0,\"reblogs_count\":0,\"favourites_count\":0,\"reblogged\":null,\"favourited\":null,\"muted\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"mentions\":[],\"tags\":[],\"card\":null,\"poll\":null,\"application\":{\"name\":\"web\",\"website\":null},\"language\":null,\"pinned\":null,\"account\":{\"id\":\"144813993922531328\",\"username\":\"Clement\",\"acct\":\"Clement\",\"display_name\":\"Andrea\",\"locked\":false,\"created_at\":\"2020-03-15T14:39:06.000000Z\",\"followers_count\":0,\"following_count\":2,\"statuses_count\":0,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Clement\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false}}},{\"id\":\"45941\",\"type\":\"mention\",\"created_at\":\"2020-03-15T14:40:52.000000Z\",\"account\":{\"id\":\"144813993922531328\",\"username\":\"Clement\",\"acct\":\"Clement\",\"display_name\":\"Andrea\",\"locked\":false,\"created_at\":\"2020-03-15T14:39:06.000000Z\",\"followers_count\":0,\"following_count\":2,\"statuses_count\":0,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Clement\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"status\":{\"id\":\"144814428691501056\",\"uri\":\"https:\\/\\/pixelfed.de\\/p\\/Clement\\/144814428691501056\",\"url\":\"https:\\/\\/pixelfed.de\\/p\\/Clement\\/144814428691501056\",\"in_reply_to_id\":144456497894658048,\"in_reply_to_account_id\":136453537340198912,\"reblog\":null,\"content\":\"@dante<\\/a> I identify to this pic.\",\"created_at\":\"2020-03-15T14:40:50.000000Z\",\"emojis\":[],\"replies_count\":0,\"reblogs_count\":0,\"favourites_count\":1,\"reblogged\":null,\"favourited\":null,\"muted\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"mentions\":[{\"id\":\"136453537340198912\",\"url\":\"https:\\/\\/pixelfed.de\\/dante\",\"username\":\"dante\",\"acct\":\"dante\"}],\"tags\":[],\"card\":null,\"poll\":null,\"application\":{\"name\":\"web\",\"website\":null},\"language\":null,\"pinned\":null,\"account\":{\"id\":\"144813993922531328\",\"username\":\"Clement\",\"acct\":\"Clement\",\"display_name\":\"Andrea\",\"locked\":false,\"created_at\":\"2020-03-15T14:39:06.000000Z\",\"followers_count\":0,\"following_count\":2,\"statuses_count\":0,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Clement\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false}}},{\"id\":\"45940\",\"type\":\"favourite\",\"created_at\":\"2020-03-15T14:40:22.000000Z\",\"account\":{\"id\":\"144813993922531328\",\"username\":\"Clement\",\"acct\":\"Clement\",\"display_name\":\"Andrea\",\"locked\":false,\"created_at\":\"2020-03-15T14:39:06.000000Z\",\"followers_count\":0,\"following_count\":2,\"statuses_count\":0,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Clement\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"status\":{\"id\":\"144456497894658048\",\"uri\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"url\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"reblog\":null,\"content\":\"Saturn V launch\",\"created_at\":\"2020-03-14T14:58:32.000000Z\",\"emojis\":[],\"replies_count\":0,\"reblogs_count\":1,\"favourites_count\":6,\"reblogged\":null,\"favourited\":null,\"muted\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"mentions\":[],\"tags\":[],\"card\":null,\"poll\":null,\"application\":{\"name\":\"web\",\"website\":null},\"language\":null,\"pinned\":null,\"account\":{\"id\":\"136453537340198912\",\"username\":\"dante\",\"acct\":\"dante\",\"display_name\":\"Dante\",\"locked\":false,\"created_at\":\"2020-02-21T12:57:38.000000Z\",\"followers_count\":3,\"following_count\":4,\"statuses_count\":1,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/dante\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"media_attachments\":[{\"id\":\"16583\",\"type\":\"image\",\"url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad.jpeg\",\"remote_url\":null,\"preview_url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad_thumb.jpeg\",\"text_url\":null,\"meta\":null,\"description\":null}]}},{\"id\":\"45939\",\"type\":\"follow\",\"created_at\":\"2020-03-15T14:40:12.000000Z\",\"account\":{\"id\":\"144813993922531328\",\"username\":\"Clement\",\"acct\":\"Clement\",\"display_name\":\"Andrea\",\"locked\":false,\"created_at\":\"2020-03-15T14:39:06.000000Z\",\"followers_count\":0,\"following_count\":2,\"statuses_count\":0,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Clement\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false}},{\"id\":\"45804\",\"type\":\"favourite\",\"created_at\":\"2020-03-15T02:22:47.000000Z\",\"account\":{\"id\":\"131984031779786752\",\"username\":\"joska\",\"acct\":\"joska\",\"display_name\":\"jxzk\",\"locked\":false,\"created_at\":\"2020-02-09T04:57:25.000000Z\",\"followers_count\":3,\"following_count\":2,\"statuses_count\":82,\"note\":\"Feliz :D\",\"url\":\"https:\\/\\/pixelfed.de\\/joska\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/013\\/198\\/403\\/177\\/978\\/675\\/2\\/zhytNrT3ij5cHBXX1mJv_avatar.jpeg?v=4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/013\\/198\\/403\\/177\\/978\\/675\\/2\\/zhytNrT3ij5cHBXX1mJv_avatar.jpeg?v=4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"status\":{\"id\":\"144456497894658048\",\"uri\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"url\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"reblog\":null,\"content\":\"Saturn V launch\",\"created_at\":\"2020-03-14T14:58:32.000000Z\",\"emojis\":[],\"replies_count\":0,\"reblogs_count\":1,\"favourites_count\":6,\"reblogged\":null,\"favourited\":null,\"muted\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"mentions\":[],\"tags\":[],\"card\":null,\"poll\":null,\"application\":{\"name\":\"web\",\"website\":null},\"language\":null,\"pinned\":null,\"account\":{\"id\":\"136453537340198912\",\"username\":\"dante\",\"acct\":\"dante\",\"display_name\":\"Dante\",\"locked\":false,\"created_at\":\"2020-02-21T12:57:38.000000Z\",\"followers_count\":3,\"following_count\":4,\"statuses_count\":1,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/dante\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"media_attachments\":[{\"id\":\"16583\",\"type\":\"image\",\"url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad.jpeg\",\"remote_url\":null,\"preview_url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad_thumb.jpeg\",\"text_url\":null,\"meta\":null,\"description\":null}]}},{\"id\":\"45783\",\"type\":\"favourite\",\"created_at\":\"2020-03-15T00:45:53.000000Z\",\"account\":{\"id\":\"139939422090170368\",\"username\":\"DrMsch\",\"acct\":\"DrMsch\",\"display_name\":\"Mische\",\"locked\":false,\"created_at\":\"2020-03-02T03:49:18.000000Z\",\"followers_count\":13,\"following_count\":21,\"statuses_count\":9,\"note\":\"TelefonFotos und Malereien von DerMische\",\"url\":\"https:\\/\\/pixelfed.de\\/DrMsch\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/013\\/993\\/942\\/209\\/017\\/036\\/8\\/xQUOq3tBNgOhFItKMZ56_avatar.jpeg?v=4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/013\\/993\\/942\\/209\\/017\\/036\\/8\\/xQUOq3tBNgOhFItKMZ56_avatar.jpeg?v=4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"status\":{\"id\":\"144456497894658048\",\"uri\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"url\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"reblog\":null,\"content\":\"Saturn V launch\",\"created_at\":\"2020-03-14T14:58:32.000000Z\",\"emojis\":[],\"replies_count\":0,\"reblogs_count\":1,\"favourites_count\":6,\"reblogged\":null,\"favourited\":null,\"muted\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"mentions\":[],\"tags\":[],\"card\":null,\"poll\":null,\"application\":{\"name\":\"web\",\"website\":null},\"language\":null,\"pinned\":null,\"account\":{\"id\":\"136453537340198912\",\"username\":\"dante\",\"acct\":\"dante\",\"display_name\":\"Dante\",\"locked\":false,\"created_at\":\"2020-02-21T12:57:38.000000Z\",\"followers_count\":3,\"following_count\":4,\"statuses_count\":1,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/dante\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"media_attachments\":[{\"id\":\"16583\",\"type\":\"image\",\"url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad.jpeg\",\"remote_url\":null,\"preview_url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad_thumb.jpeg\",\"text_url\":null,\"meta\":null,\"description\":null}]}},{\"id\":\"45768\",\"type\":\"favourite\",\"created_at\":\"2020-03-14T22:43:18.000000Z\",\"account\":{\"id\":\"139819612522024960\",\"username\":\"vitorpires\",\"acct\":\"vitorpires\",\"display_name\":\"Vitor Pires\",\"locked\":false,\"created_at\":\"2020-03-01T19:53:13.000000Z\",\"followers_count\":20,\"following_count\":8,\"statuses_count\":42,\"note\":\"photography\\/graphic design\\/desktop publishing\\/illustration\\/3D animation\\/video\\/content writing\\/teaching\\/woodworking\\/luthier\",\"url\":\"https:\\/\\/pixelfed.de\\/vitorpires\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/013\\/981\\/961\\/252\\/202\\/496\\/0\\/2HB6Gs2m5NaSys7W5ikG_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/013\\/981\\/961\\/252\\/202\\/496\\/0\\/2HB6Gs2m5NaSys7W5ikG_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"status\":{\"id\":\"144456497894658048\",\"uri\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"url\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"reblog\":null,\"content\":\"Saturn V launch\",\"created_at\":\"2020-03-14T14:58:32.000000Z\",\"emojis\":[],\"replies_count\":0,\"reblogs_count\":1,\"favourites_count\":6,\"reblogged\":null,\"favourited\":null,\"muted\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"mentions\":[],\"tags\":[],\"card\":null,\"poll\":null,\"application\":{\"name\":\"web\",\"website\":null},\"language\":null,\"pinned\":null,\"account\":{\"id\":\"136453537340198912\",\"username\":\"dante\",\"acct\":\"dante\",\"display_name\":\"Dante\",\"locked\":false,\"created_at\":\"2020-02-21T12:57:38.000000Z\",\"followers_count\":3,\"following_count\":4,\"statuses_count\":1,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/dante\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"media_attachments\":[{\"id\":\"16583\",\"type\":\"image\",\"url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad.jpeg\",\"remote_url\":null,\"preview_url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad_thumb.jpeg\",\"text_url\":null,\"meta\":null,\"description\":null}]}},{\"id\":\"45723\",\"type\":\"favourite\",\"created_at\":\"2020-03-14T15:01:49.000000Z\",\"account\":{\"id\":\"79574199701737472\",\"username\":\"Spaziergaenger\",\"acct\":\"Spaziergaenger\",\"display_name\":\"anonymous\",\"locked\":false,\"created_at\":\"2019-09-17T13:59:27.000000Z\",\"followers_count\":40,\"following_count\":0,\"statuses_count\":894,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/Spaziergaenger\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/007\\/957\\/419\\/970\\/173\\/747\\/2\\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/007\\/957\\/419\\/970\\/173\\/747\\/2\\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"status\":{\"id\":\"144456497894658048\",\"uri\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"url\":\"https:\\/\\/pixelfed.de\\/p\\/dante\\/144456497894658048\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"reblog\":null,\"content\":\"Saturn V launch\",\"created_at\":\"2020-03-14T14:58:32.000000Z\",\"emojis\":[],\"replies_count\":0,\"reblogs_count\":1,\"favourites_count\":6,\"reblogged\":null,\"favourited\":null,\"muted\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"mentions\":[],\"tags\":[],\"card\":null,\"poll\":null,\"application\":{\"name\":\"web\",\"website\":null},\"language\":null,\"pinned\":null,\"account\":{\"id\":\"136453537340198912\",\"username\":\"dante\",\"acct\":\"dante\",\"display_name\":\"Dante\",\"locked\":false,\"created_at\":\"2020-02-21T12:57:38.000000Z\",\"followers_count\":3,\"following_count\":4,\"statuses_count\":1,\"note\":\"\",\"url\":\"https:\\/\\/pixelfed.de\\/dante\",\"avatar\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"avatar_static\":\"https:\\/\\/pixelfed.de\\/storage\\/avatars\\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9\",\"header\":\"\",\"header_static\":\"\",\"emojis\":[],\"moved\":null,\"fields\":null,\"bot\":false,\"software\":\"pixelfed\",\"is_admin\":false},\"media_attachments\":[{\"id\":\"16583\",\"type\":\"image\",\"url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad.jpeg\",\"remote_url\":null,\"preview_url\":\"https:\\/\\/pixelfed.de\\/storage\\/m\\/113a3e2124a33b1f5511e531953f5ee48456e0c7\\/0fa8bbe19cc23442034913a7c97fbe4527c1d63a\\/vs2vouJ86OvzxhK9ewhPlfPf4Y9IoQ5CHfiBIqad_thumb.jpeg\",\"text_url\":null,\"meta\":null,\"description\":null}]}}]" - const val accountStatusesJson = """[{"id":"153848799696130048","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153848799696130048","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153848799696130048","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nn\u00e4her geht es an Neu-Bamberg und den dritten Kontrollpunkt heran","created_at":"2020-04-09T13:00:12.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":1,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18521","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/IX5g6bLSCB4lpp0wTr6HKj4mYEpNUhgrjv50InJ3.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/IX5g6bLSCB4lpp0wTr6HKj4mYEpNUhgrjv50InJ3_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"153848420002566144","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153848420002566144","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153848420002566144","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nimmer wieder Wein und im Hintergrund der Steinbruch","created_at":"2020-04-09T12:58:41.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":1,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18520","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/g0ScH8shkytKjS9fask1CZHj0TMzWhbgJALApt5u.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/g0ScH8shkytKjS9fask1CZHj0TMzWhbgJALApt5u_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"153848196064481280","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153848196064481280","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153848196064481280","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\ndas Ende der Steigung ist absehbar","created_at":"2020-04-09T12:57:48.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":0,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18518","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/xLvyeeywOdeUTxYW03caxRXzNr4bldmAB1JUXUzv.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/xLvyeeywOdeUTxYW03caxRXzNr4bldmAB1JUXUzv_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"153446932994461696","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153446932994461696","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153446932994461696","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nweiter gehts es steil den Berg hinauf","created_at":"2020-04-08T10:23:19.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":0,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18421","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/VkG4AxsEE8gyWqUxprpna6LNSwRtKjW6A8yoZijI.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/VkG4AxsEE8gyWqUxprpna6LNSwRtKjW6A8yoZijI_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"153446615137521664","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153446615137521664","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153446615137521664","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nUnd hier gibt es neben dem zweiten Stempel auch k\u00fchle Getr\u00e4nke und Reibekuchen zum Sattessen","created_at":"2020-04-08T10:22:03.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":0,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18420","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/BhSqEKyy6iBlJGnrHpvjp2ZCrmCnepM0E3c83vvf.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/BhSqEKyy6iBlJGnrHpvjp2ZCrmCnepM0E3c83vvf_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"153446167173271552","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153446167173271552","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153446167173271552","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nDer zweite Kontrollpunkt: das sogenannte Wingertsh\u00e4uschen","created_at":"2020-04-08T10:20:17.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":4,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18419","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/iPjOwRResYI4BZxSNV7ZNpTsLz7Wna4IRHcu25dz.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/iPjOwRResYI4BZxSNV7ZNpTsLz7Wna4IRHcu25dz_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"153053485812813824","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153053485812813824","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153053485812813824","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nBlick auf die Kreisstra\u00dfe 88","created_at":"2020-04-07T08:19:54.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":2,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18291","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/GdQiZxDAB99NYbq0QpeW4GoNDzhAGuxcylOhWaBF.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/GdQiZxDAB99NYbq0QpeW4GoNDzhAGuxcylOhWaBF_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"153053256745095168","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153053256745095168","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153053256745095168","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nendlich Schatten","created_at":"2020-04-07T08:18:59.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":3,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18290","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/6TIe7Rj5vLFwp7VIRjLapnLqOzQ0WCtfnaVni9QS.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/6TIe7Rj5vLFwp7VIRjLapnLqOzQ0WCtfnaVni9QS_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"153052983712681984","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153052983712681984","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/153052983712681984","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nDer Wald kommt immer n\u00e4her","created_at":"2020-04-07T08:17:54.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":4,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18285","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/N0Dmh4lwjTcI8ACNqnxsS2cuWN5ZvusnZAt8XauE.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/N0Dmh4lwjTcI8ACNqnxsS2cuWN5ZvusnZAt8XauE_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"152657825544409088","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152657825544409088","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152657825544409088","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nnoch immer bei glei\u00dfender Sonne auf offenem Feld kommt Wald in Sicht","created_at":"2020-04-06T06:07:41.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":3,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18180","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/jgnP7gR3rw37yCnF1T5379krsZhRzh6JHiASINSP.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/jgnP7gR3rw37yCnF1T5379krsZhRzh6JHiASINSP_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"152657278993043456","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152657278993043456","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152657278993043456","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nBlick durch die Zweige auf die historische Kapelle von Hof Iben","created_at":"2020-04-06T06:05:31.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":2,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18179","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/FyadKaDMpUZkde9cQzrBKbtcAaHKsT4WtDQSnLMg.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/FyadKaDMpUZkde9cQzrBKbtcAaHKsT4WtDQSnLMg_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"152656864235098112","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152656864235098112","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152656864235098112","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nimmer weiter \u00fcber offene Felder","created_at":"2020-04-06T06:03:52.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":4,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18178","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/ExqK24a3Ri3Jn7XEodkoAx7kRLJIQPuWaFzx09Sk.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/ExqK24a3Ri3Jn7XEodkoAx7kRLJIQPuWaFzx09Sk_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"152158673148448768","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152158673148448768","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152158673148448768","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\n... und r\u00fcber nach Neu Bamberg, links der Steinbruch","created_at":"2020-04-04T21:04:14.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":1,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18106","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/r29IsMP9TUagGBhr8UzRi7Odb83mMJNotEAZxqsS.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/r29IsMP9TUagGBhr8UzRi7Odb83mMJNotEAZxqsS_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"152158370521026560","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152158370521026560","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152158370521026560","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nDer Blick zur\u00fcck nach F\u00fcrfeld ...","created_at":"2020-04-04T21:03:02.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":2,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18105","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/CDMIhBX34DlG3L4clpHQvdPoBpR8MCgIUSy1cnQU.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/CDMIhBX34DlG3L4clpHQvdPoBpR8MCgIUSy1cnQU_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"152157823550230528","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152157823550230528","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/152157823550230528","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\n... bis zum Horizont ...","created_at":"2020-04-04T21:00:52.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":1,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18104","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/JYBeWD99cXadqo385uZjH98y2Cs1tQ0kun1NH9vd.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/JYBeWD99cXadqo385uZjH98y2Cs1tQ0kun1NH9vd_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"151795147183624192","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151795147183624192","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151795147183624192","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nDuch die Weinberge Richtung Hof Iben","created_at":"2020-04-03T20:59:43.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":1,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18071","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/VUD4ewNlZ7ofpFiO8wG9SEoaG4TbmRBgNtfEkEKw.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/VUD4ewNlZ7ofpFiO8wG9SEoaG4TbmRBgNtfEkEKw_thumb.jpeg","text_url":null,"meta":null,"description":null},{"id":"18072","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/2tiRGDebac4DvE3a2djRjCHtaJuspwdyaxiu27JY.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/2tiRGDebac4DvE3a2djRjCHtaJuspwdyaxiu27JY_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"151794839434956800","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151794839434956800","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151794839434956800","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nBlick nach S\u00fcden \u00fcber die herrliche Landschaft","created_at":"2020-04-03T20:58:29.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":1,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18070","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/vdEd5cyW6SMxectXdDVPdMNNt4TNrtERHSRGWc9R.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/vdEd5cyW6SMxectXdDVPdMNNt4TNrtERHSRGWc9R_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"151794394431885312","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151794394431885312","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151794394431885312","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nBlick \u00fcber F\u00fcrfeld","created_at":"2020-04-03T20:56:43.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":4,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"18069","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/sGtZi6f3Q9cJfc6cou08otJqBoGCcSlot64yttra.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/sGtZi6f3Q9cJfc6cou08otJqBoGCcSlot64yttra_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"151344455130157056","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151344455130157056","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151344455130157056","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nDas Fahrzeug der Veranstaltungsleitung von hinten","created_at":"2020-04-02T15:08:49.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":2,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"17966","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/0bixTrcD8FKkM12kG7k21OzfDDZQIVEyadu7Tv47.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/0bixTrcD8FKkM12kG7k21OzfDDZQIVEyadu7Tv47_thumb.jpeg","text_url":null,"meta":null,"description":null}]},{"id":"151343830426324992","uri":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151343830426324992","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/151343830426324992","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"2017-05-28 VG Bad-Kreuznach Wandertag
\n
\nBlick zur\u00fcck zum Eichelberg","created_at":"2020-04-02T15:06:21.000000Z","emojis":[],"replies_count":0,"reblogs_count":0,"favourites_count":1,"reblogged":null,"favourited":null,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","mentions":[],"tags":[],"card":null,"poll":null,"application":{"name":"web","website":null},"language":null,"pinned":null,"account":{"id":"79574199701737472","username":"Spaziergaenger","acct":"Spaziergaenger","display_name":"anonymous","locked":false,"created_at":"2019-09-17T13:59:27.000000Z","followers_count":43,"following_count":0,"statuses_count":966,"note":"","url":"https:\/\/pixelfed.de\/Spaziergaenger","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/007\/957\/419\/970\/173\/747\/2\/KEg4YgCgsmzdgyVztszz_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"17965","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/PgvSJ25k9SS6dhFh5l3v0rgQ5r7SY5JbepkrDkPP.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/PgvSJ25k9SS6dhFh5l3v0rgQ5r7SY5JbepkrDkPP_thumb.jpeg","text_url":null,"meta":null,"description":null}]}]""" - const val commentStatusesJson = """{ - "ancestors": [], - "descendants": [ - { - "id": "103189026958574542", - "created_at": "2019-11-23T20:06:36.011Z", - "in_reply_to_id": "140364967936397312", - "in_reply_to_account_id": null, - "sensitive": false, - "spoiler_text": "", - "visibility": "public", - "language": "en", - "uri": "https://mastodon.social/users/Gargron/statuses/103270115826048975", - "url": "https://mastodon.social/@Gargron/103270115826048975", - "replies_count": 5, - "reblogs_count": 6, - "favourites_count": 11, - "favourited": false, - "reblogged": false, - "muted": false, - "bookmarked": false, - "content": "

"I lost my inheritance with one wrong digit on my sort code"

https://www.theguardian.com/money/2019/dec/07/i-lost-my-193000-inheritance-with-one-wrong-digit-on-my-sort-code", - "reblog": null, - "application": { - "name": "Web", - "website": null - }, - "account": { - "id": "1", - "username": "Gargron", - "acct": "Gargron", - "display_name": "Eugen", - "locked": false, - "bot": false, - "discoverable": true, - "group": false, - "created_at": "2016-03-16T14:34:26.392Z", - "note": "

Developer of Mastodon and administrator of mastodon.social. I post service announcements, development updates, and personal stuff.

", - "url": "https://mastodon.social/@Gargron", - "avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg", - "avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/d96d39a0abb45b92.jpg", - "header": "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png", - "header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png", - "followers_count": 322930, - "following_count": 459, - "statuses_count": 61323, - "last_status_at": "2019-12-10T08:14:44.811Z", - "emojis": [], - "fields": [ - { - "name": "Patreon", - "value": "https://www.patreon.com/mastodonhttps://zeonfederated.com@Dobios<\/a> @Dante<\/a>","reblog":null,"application":{"name":"web","website":null},"mentions":[{"id":"136800034732773376","url":"https:\/\/pixelfed.de\/Dobios","username":"Dobios","acct":"Dobios"},{"id":"136453537340198912","url":"https:\/\/pixelfed.de\/dante","username":"dante","acct":"dante"}],"tags":[{"name":"mushroom","url":"https:\/\/pixelfed.de\/discover\/tags\/mushroom"},{"name":"commentsstillbroken","url":"https:\/\/pixelfed.de\/discover\/tags\/commentsstillbroken"},{"name":"fixyourapi","url":"https:\/\/pixelfed.de\/discover\/tags\/fixyourapi"},{"name":"pls","url":"https:\/\/pixelfed.de\/discover\/tags\/pls"}],"emojis":[],"card":null,"poll":null,"account":{"id":"145183325781364736","username":"machintuck","acct":"machintuck","display_name":"Arthur","locked":false,"created_at":"2020-03-16T15:06:42.000000Z","followers_count":4,"following_count":4,"statuses_count":5,"note":"","url":"https:\/\/pixelfed.de\/machintuck","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/014\/518\/332\/578\/136\/473\/6\/gbdKtKOhTkNA5UxCzeAQ_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},"media_attachments":[{"id":"19228","type":"image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/9b4393bfd32c643a265bd1c557b981f167d60969\/lbOqQOMeHLGmhYgehhZUBJ4JvjtKulh83BA97LoP_thumb.jpeg","text_url":null,"meta":null,"description":null}]}""" - - - const val followRelationshipJson = """{"id":"136800034732773376","following":true,"followed_by":true,"blocking":false,"muting":false,"muting_notifications":null,"requested":false,"domain_blocking":null,"showing_reblogs":null,"endorsed":false}""" - const val unfollowRelationshipJson = """{"id":"136800034732773376","following":false,"followed_by":true,"blocking":false,"muting":false,"muting_notifications":null,"requested":false,"domain_blocking":null,"showing_reblogs":null,"endorsed":false}""" - const val relationshipJson = """[{"id":"136800034732773376","following":true,"followed_by":true,"blocking":false,"muting":false,"muting_notifications":null,"requested":false,"domain_blocking":null,"showing_reblogs":null,"endorsed":false}]""" - const val followersJson = """[{"id":"118664651939647488","username":"ete2","acct":"ete2","display_name":"Christian","locked":false,"created_at":"2020-01-03T10:50:57.000000Z","followers_count":22,"following_count":3,"statuses_count":20,"note":"Nature lover - hobby photographer","url":"https:\/\/pixelfed.de\/ete2","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/011\/866\/465\/193\/964\/748\/8\/pUY03jBlOeOqjNDMWwYM_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/011\/866\/465\/193\/964\/748\/8\/pUY03jBlOeOqjNDMWwYM_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},{"id":"136800034732773376","username":"Dobios","acct":"Dobios","display_name":"Andrew Dobis","locked":false,"created_at":"2020-02-22T11:54:29.000000Z","followers_count":7,"following_count":5,"statuses_count":3,"note":"","url":"https:\/\/pixelfed.de\/Dobios","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/013\/680\/003\/473\/277\/337\/6\/AnBpDi92CAuuNjOYkyqg_avatar.jpeg?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/013\/680\/003\/473\/277\/337\/6\/AnBpDi92CAuuNjOYkyqg_avatar.jpeg?v=4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false},{"id":"144813993922531328","username":"Clement","acct":"Clement","display_name":"Andrea","locked":false,"created_at":"2020-03-15T14:39:06.000000Z","followers_count":2,"following_count":4,"statuses_count":0,"note":"","url":"https:\/\/pixelfed.de\/Clement","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9","avatar_static":"https:\/\/pixelfed.de\/storage\/avatars\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9","header":"","header_static":"","emojis":[],"moved":null,"fields":null,"bot":false,"software":"pixelfed","is_admin":false}]""" - const val followersAfterJson = """[]""" - const val searchCaturday = """{"accounts":[],"hashtags":[{"name":"caturday","url":"https:\/\/pixelfed.de\/discover\/tags\/caturday","history":[]}],"statuses":[{"id":"159936348634091520","uri":"https:\/\/pixelfed.de\/p\/memo\/159936348634091520","url":"https:\/\/pixelfed.de\/p\/memo\/159936348634091520","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"Kater quer - ist manchmal recht schwer \ud83d\ude04
\n
\n
#smartography<\/a> #smartografie<\/a> #photography<\/a> #fotografie<\/a> #foto<\/a> #photo<\/a> #postprocessed<\/a> #mywork<\/a> #ownwork<\/a> #bauwagen<\/a> #memovillage<\/a> #caturday<\/a>","content_text":"Kater quer - ist manchmal recht schwer \ud83d\ude04\n\n#smartography #smartografie #photography #fotografie #foto #photo #postprocessed #mywork #ownwork #bauwagen #memovillage #caturday","created_at":"2020-04-26T08:09:56+00:00","emojis":[],"reblogs_count":1,"favourites_count":6,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":0,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"144086892776919040","username":"memo","acct":"memo","display_name":"Memo","locked":false,"followers_count":33,"following_count":11,"statuses_count":59,"note":"Ein 7m\u00b2 Bauwagen bietet mir gen\u00fcgend Platz f\u00fcr die Meditation und dem Studium buddhistischer Schriften.","url":"https:\/\/pixelfed.de\/memo","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/408\/689\/277\/691\/904\/0\/pC4CZV0OmSHGfpsRl9MX_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":null,"local":true,"is_admin":false,"created_at":"2020-03-13T14:29:52.000000Z","header_bg":"mauve"},"media_attachments":[{"id":"20282","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/iQkWLvASx473DxmgioN6qd6ExOiqFqjYQMYOCHPn.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/iQkWLvASx473DxmgioN6qd6ExOiqFqjYQMYOCHPn_thumb.jpeg","text_url":null,"meta":null,"description":"Kater quer - ist manchmal recht schwer \ud83d\ude04","license":null,"is_nsfw":0,"orientation":"landscape","filter_name":null,"filter_class":null,"mime":"image\/jpeg"}]},{"id":"159603005711323136","uri":"https:\/\/pixelfed.de\/p\/memo\/159603005711323136","url":"https:\/\/pixelfed.de\/p\/memo\/159603005711323136","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"Och n\u00f6, nicht schon wieder #caturday<\/a>
\n
\n
#smartography<\/a> #smartografie<\/a> #photography<\/a> #fotografie<\/a> #foto<\/a> #photo<\/a> #postprocessed<\/a> #mywork<\/a> #ownwork<\/a> #bauwagen<\/a> #memovillage<\/a>","content_text":"Och n\u00f6, nicht schon wieder #caturday\n\n#smartography #smartografie #photography #fotografie #foto #photo #postprocessed #mywork #ownwork #bauwagen #memovillage","created_at":"2020-04-25T10:05:21+00:00","emojis":[],"reblogs_count":1,"favourites_count":6,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":0,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"144086892776919040","username":"memo","acct":"memo","display_name":"Memo","locked":false,"followers_count":33,"following_count":11,"statuses_count":59,"note":"Ein 7m\u00b2 Bauwagen bietet mir gen\u00fcgend Platz f\u00fcr die Meditation und dem Studium buddhistischer Schriften.","url":"https:\/\/pixelfed.de\/memo","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/408\/689\/277\/691\/904\/0\/pC4CZV0OmSHGfpsRl9MX_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":null,"local":true,"is_admin":false,"created_at":"2020-03-13T14:29:52.000000Z","header_bg":"mauve"},"media_attachments":[{"id":"20150","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/d8kYeRmrvLdE2lAEWilyIHjn7HZpytiVkfTsYpR3.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/d8kYeRmrvLdE2lAEWilyIHjn7HZpytiVkfTsYpR3_thumb.jpeg","text_url":null,"meta":null,"description":"Och n\u00f6, nicht schon wieder caturday","license":null,"is_nsfw":0,"orientation":"landscape","filter_name":null,"filter_class":null,"mime":"image\/jpeg"}]},{"id":"157763505968254976","uri":"https:\/\/pixelfed.de\/p\/memo\/157763505968254976","url":"https:\/\/pixelfed.de\/p\/memo\/157763505968254976","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"\u00c4rgerlich, wenn der Platz schon besetzt ist \ud83d\ude06
\n
\n
#smartography<\/a> #smartografie<\/a> #photography<\/a> #fotografie<\/a> #foto<\/a> #photo<\/a> #postprocessed<\/a> #mywork<\/a> #ownwork<\/a> #memovillage<\/a> #caturday<\/a>","content_text":"\u00c4rgerlich, wenn der Platz schon besetzt ist \ud83d\ude06\n\n#smartography #smartografie #photography #fotografie #foto #photo #postprocessed #mywork #ownwork #memovillage #caturday","created_at":"2020-04-20T08:15:50+00:00","emojis":[],"reblogs_count":1,"favourites_count":7,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":1,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"144086892776919040","username":"memo","acct":"memo","display_name":"Memo","locked":false,"followers_count":33,"following_count":11,"statuses_count":59,"note":"Ein 7m\u00b2 Bauwagen bietet mir gen\u00fcgend Platz f\u00fcr die Meditation und dem Studium buddhistischer Schriften.","url":"https:\/\/pixelfed.de\/memo","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/408\/689\/277\/691\/904\/0\/pC4CZV0OmSHGfpsRl9MX_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":null,"local":true,"is_admin":false,"created_at":"2020-03-13T14:29:52.000000Z","header_bg":"mauve"},"media_attachments":[{"id":"19539","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/TVzm85vohJq8hsWcYuADeVIophCPrrKNCPVWU92I.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/TVzm85vohJq8hsWcYuADeVIophCPrrKNCPVWU92I_thumb.jpeg","text_url":null,"meta":null,"description":"\u00c4rgerlich, wenn der Platz schon besetzt ist \ud83d\ude06","license":null,"is_nsfw":0,"orientation":"landscape","filter_name":null,"filter_class":null,"mime":"image\/jpeg"}]},{"id":"157164321040896000","uri":"https:\/\/pixelfed.de\/p\/memo\/157164321040896000","url":"https:\/\/pixelfed.de\/p\/memo\/157164321040896000","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"Ich habe zwar \u00fcberhaupt nichts getan, aber ich mache dann auch mal Pause \ud83d\ude04
\n
\n
#smartography<\/a> #smartografie<\/a> #photography<\/a> #fotografie<\/a> #foto<\/a> #photo<\/a> #postprocessed<\/a> #mywork<\/a> #ownwork<\/a> #memovillage<\/a> #caturday<\/a>","content_text":"Ich habe zwar \u00fcberhaupt nichts getan, aber ich mache dann auch mal Pause \ud83d\ude04\n\n#smartography #smartografie #photography #fotografie #foto #photo #postprocessed #mywork #ownwork #memovillage #caturday","created_at":"2020-04-18T16:34:54+00:00","emojis":[],"reblogs_count":0,"favourites_count":4,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":0,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"144086892776919040","username":"memo","acct":"memo","display_name":"Memo","locked":false,"followers_count":33,"following_count":11,"statuses_count":59,"note":"Ein 7m\u00b2 Bauwagen bietet mir gen\u00fcgend Platz f\u00fcr die Meditation und dem Studium buddhistischer Schriften.","url":"https:\/\/pixelfed.de\/memo","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/408\/689\/277\/691\/904\/0\/pC4CZV0OmSHGfpsRl9MX_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":null,"local":true,"is_admin":false,"created_at":"2020-03-13T14:29:52.000000Z","header_bg":"mauve"},"media_attachments":[{"id":"19392","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/Asys2NMtMrsJW7JopEEgYMhn08MgvdzZAdXC24HL.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/Asys2NMtMrsJW7JopEEgYMhn08MgvdzZAdXC24HL_thumb.jpeg","text_url":null,"meta":null,"description":"Ich habe zwar \u00fcberhaupt nichts getan, aber ich mache dann auch mal Pause \ud83d\ude04","license":null,"is_nsfw":0,"orientation":"landscape","filter_name":null,"filter_class":null,"mime":"image\/jpeg"}]},{"id":"154909082959089664","uri":"https:\/\/pixelfed.de\/p\/memo\/154909082959089664","url":"https:\/\/pixelfed.de\/p\/memo\/154909082959089664","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"Memo-Village ist f\u00fcr alle Wesen ein Ort der Ruhe \ud83d\ude00
\n
\n
#smartography<\/a> #smartografie<\/a> #photography<\/a> #fotografie<\/a> #foto<\/a> #photo<\/a> #postprocessed<\/a> #mywork<\/a> #ownwork<\/a> #bauwagen<\/a> #memovillage<\/a> #caturday<\/a>","content_text":"Memo-Village ist f\u00fcr alle Wesen ein Ort der Ruhe \ud83d\ude00\n\n#smartography #smartografie #photography #fotografie #foto #photo #postprocessed #mywork #ownwork #bauwagen #memovillage #caturday","created_at":"2020-04-12T11:13:23+00:00","emojis":[],"reblogs_count":0,"favourites_count":8,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":1,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"144086892776919040","username":"memo","acct":"memo","display_name":"Memo","locked":false,"followers_count":33,"following_count":11,"statuses_count":59,"note":"Ein 7m\u00b2 Bauwagen bietet mir gen\u00fcgend Platz f\u00fcr die Meditation und dem Studium buddhistischer Schriften.","url":"https:\/\/pixelfed.de\/memo","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/408\/689\/277\/691\/904\/0\/pC4CZV0OmSHGfpsRl9MX_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":null,"local":true,"is_admin":false,"created_at":"2020-03-13T14:29:52.000000Z","header_bg":"mauve"},"media_attachments":[{"id":"18713","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/tgpkXES9ySJDmkUTMICN6rF5GD7CdIguxXtPJpn0.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/tgpkXES9ySJDmkUTMICN6rF5GD7CdIguxXtPJpn0_thumb.jpeg","text_url":null,"meta":null,"description":"Memo-Village ist f\u00fcr alle Wesen ein Ort der Ruhe \ud83d\ude00","license":null,"is_nsfw":0,"orientation":"landscape","filter_name":null,"filter_class":null,"mime":"image\/jpeg"}]},{"id":"154526434457686016","uri":"https:\/\/pixelfed.de\/p\/memo\/154526434457686016","url":"https:\/\/pixelfed.de\/p\/memo\/154526434457686016","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"Total entspannt \ud83d\ude00
\n
\n
#smartography<\/a> #smartografie<\/a> #photography<\/a> #fotografie<\/a> #foto<\/a> #photo<\/a> #postprocessed<\/a> #mywork<\/a> #ownwork<\/a> #bauwagen<\/a> #memovillage<\/a> #caturday<\/a>","content_text":"Total entspannt \ud83d\ude00\n\n#smartography #smartografie #photography #fotografie #foto #photo #postprocessed #mywork #ownwork #bauwagen #memovillage #caturday","created_at":"2020-04-11T09:52:52+00:00","emojis":[],"reblogs_count":3,"favourites_count":9,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":0,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"144086892776919040","username":"memo","acct":"memo","display_name":"Memo","locked":false,"followers_count":33,"following_count":11,"statuses_count":59,"note":"Ein 7m\u00b2 Bauwagen bietet mir gen\u00fcgend Platz f\u00fcr die Meditation und dem Studium buddhistischer Schriften.","url":"https:\/\/pixelfed.de\/memo","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/408\/689\/277\/691\/904\/0\/pC4CZV0OmSHGfpsRl9MX_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":null,"local":true,"is_admin":false,"created_at":"2020-03-13T14:29:52.000000Z","header_bg":"mauve"},"media_attachments":[{"id":"18631","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/OghyUzygX0NbAIcCExp1aAmC1afm86bEekG3Uk4r.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/OghyUzygX0NbAIcCExp1aAmC1afm86bEekG3Uk4r_thumb.jpeg","text_url":null,"meta":null,"description":"Total entspannt \ud83d\ude00","license":null,"is_nsfw":0,"orientation":"landscape","filter_name":null,"filter_class":null,"mime":"image\/jpeg"}]},{"id":"152352374571274240","uri":"https:\/\/pixelfed.de\/p\/memo\/152352374571274240","url":"https:\/\/pixelfed.de\/p\/memo\/152352374571274240","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"Erst fressen und dann streicheln, oder andersherum? \ud83d\ude00
\n
\n
#smartography<\/a> #smartografie<\/a> #photography<\/a> #fotografie<\/a> #foto<\/a> #photo<\/a> #postprocessed<\/a> #mywork<\/a> #ownwork<\/a> #bauwagen<\/a> #caturday<\/a>","content_text":"Erst fressen und dann streicheln, oder andersherum? \ud83d\ude00\n\n#smartography #smartografie #photography #fotografie #foto #photo #postprocessed #mywork #ownwork #bauwagen #caturday","created_at":"2020-04-05T09:53:56+00:00","emojis":[],"reblogs_count":1,"favourites_count":6,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":0,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"144086892776919040","username":"memo","acct":"memo","display_name":"Memo","locked":false,"followers_count":33,"following_count":11,"statuses_count":59,"note":"Ein 7m\u00b2 Bauwagen bietet mir gen\u00fcgend Platz f\u00fcr die Meditation und dem Studium buddhistischer Schriften.","url":"https:\/\/pixelfed.de\/memo","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/408\/689\/277\/691\/904\/0\/pC4CZV0OmSHGfpsRl9MX_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":null,"local":true,"is_admin":false,"created_at":"2020-03-13T14:29:52.000000Z","header_bg":"mauve"},"media_attachments":[{"id":"18149","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/UbURQ2oHNrCpndb2gZF3AzmMB4wZ05VdvGQRUptG.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/UbURQ2oHNrCpndb2gZF3AzmMB4wZ05VdvGQRUptG_thumb.jpeg","text_url":null,"meta":null,"description":"Erst fressen und dann streicheln, oder andersherum? \ud83d\ude00","license":null,"is_nsfw":0,"orientation":"landscape","filter_name":null,"filter_class":null,"mime":"image\/jpeg"}]},{"id":"152057487443496960","uri":"https:\/\/pixelfed.de\/p\/memo\/152057487443496960","url":"https:\/\/pixelfed.de\/p\/memo\/152057487443496960","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"Eines dieser Wesen, die bei mir regelm\u00e4\u00dfig vorbeikommen, und sich Streicheleinheiten abholen \ud83d\ude00
\n
\n
#smartography<\/a> #smartografie<\/a> #photography<\/a> #fotografie<\/a> #foto<\/a> #photo<\/a> #postprocessed<\/a> #mywork<\/a> #ownwork<\/a> #bauwagen<\/a> #caturday<\/a>","content_text":"Eines dieser Wesen, die bei mir regelm\u00e4\u00dfig vorbeikommen, und sich Streicheleinheiten abholen \ud83d\ude00\n\n#smartography #smartografie #photography #fotografie #foto #photo #postprocessed #mywork #ownwork #bauwagen #caturday","created_at":"2020-04-04T14:22:10+00:00","emojis":[],"reblogs_count":0,"favourites_count":6,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":0,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"144086892776919040","username":"memo","acct":"memo","display_name":"Memo","locked":false,"followers_count":33,"following_count":11,"statuses_count":59,"note":"Ein 7m\u00b2 Bauwagen bietet mir gen\u00fcgend Platz f\u00fcr die Meditation und dem Studium buddhistischer Schriften.","url":"https:\/\/pixelfed.de\/memo","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/014\/408\/689\/277\/691\/904\/0\/pC4CZV0OmSHGfpsRl9MX_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":null,"local":true,"is_admin":false,"created_at":"2020-03-13T14:29:52.000000Z","header_bg":"mauve"},"media_attachments":[{"id":"18086","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/Qu9Tr7RmFGMfhZWTrXFKMUy5gfEls8eGRAk7KC1D.jpeg","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/f688fca6e37074bf7440c93f34de4ca47fbde5c5\/Qu9Tr7RmFGMfhZWTrXFKMUy5gfEls8eGRAk7KC1D_thumb.jpeg","text_url":null,"meta":null,"description":"Eines dieser Wesen, die bei mir regelm\u00e4\u00dfig vorbeikommen, und sich Streicheleinheiten abholen \ud83d\ude00","license":null,"is_nsfw":0,"orientation":"landscape","filter_name":null,"filter_class":null,"mime":"image\/jpeg"}]},{"id":"131821413018505216","uri":"https:\/\/pixelfed.de\/p\/hendersonb\/131821413018505216","url":"https:\/\/pixelfed.de\/p\/hendersonb\/131821413018505216","in_reply_to_id":null,"in_reply_to_account_id":null,"reblog":null,"content":"This is Ygor #cats<\/a> #caturday<\/a>","content_text":"This is Ygor #cats #caturday","created_at":"2020-02-08T18:11:13+00:00","emojis":[],"reblogs_count":0,"favourites_count":3,"reblogged":false,"favourited":false,"muted":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"web","website":null},"language":null,"pinned":null,"mentions":[],"tags":[],"pf_type":"photo","reply_count":0,"comments_disabled":false,"thread":false,"replies":[],"parent":[],"place":null,"local":true,"account":{"id":"131496536109617152","username":"hendersonb","acct":"hendersonb","display_name":"Henderson Bariani","locked":false,"followers_count":2,"following_count":4,"statuses_count":3,"note":"Somente um contador de hist\u00f3rias. Algumas s\u00e3o boas, eu juro.","url":"https:\/\/pixelfed.de\/hendersonb","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/013\/149\/653\/610\/961\/715\/2\/dsDUtTWnblOdyZzKc5fO_avatar.jpeg?v=d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35","website":"https:\/\/allmylinks.com\/hbariani","local":true,"is_admin":false,"created_at":"2020-02-07T20:40:17.000000Z","header_bg":null},"media_attachments":[{"id":"14198","type":"Image","url":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/0b0c383a8c3a80f2d4eea825387123e17839a9b1\/cQvkmbKxDdFftGWncg8DMq2u7QOc5yQgTd6LCm2F.png","remote_url":null,"preview_url":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/0b0c383a8c3a80f2d4eea825387123e17839a9b1\/cQvkmbKxDdFftGWncg8DMq2u7QOc5yQgTd6LCm2F_thumb.png","text_url":null,"meta":null,"description":null,"license":null,"is_nsfw":0,"orientation":"portrait","filter_name":null,"filter_class":null,"mime":"image\/png"}]}]}""" - const val searchCaturdayHashtags = """{"accounts":[],"hashtags":[{"name":"caturday","url":"https:\/\/pixelfed.de\/discover\/tags\/caturday","history":[]}],"statuses":[]}""" - const val searchEmpty = """{"accounts":[],"hashtags":[],"statuses":[]}""" - const val searchDansupAccounts = """{"accounts":[{"id":"1238","username":"dansup","acct":"dansup@pixelfed.social","display_name":"dansup","locked":false,"followers_count":14,"following_count":0,"statuses_count":0,"note":"Hi, I'm the developer behind Pixelfed!","url":"https:\/\/pixelfed.social\/users\/dansup","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9","website":null,"local":false,"is_admin":false,"created_at":"2019-04-19T01:14:56.000000Z","header_bg":null},{"id":"78120743388450816","username":"dansup","acct":"dansup@mastodon.social","display_name":"dansup\u00ae","locked":false,"followers_count":2,"following_count":0,"statuses_count":1,"note":"

Full stack developer.<\/p>

Creator of @pixelfed<\/span><\/a><\/span>, admin of https:\/\/<\/span>pixelfed.social<\/span><\/span><\/a><\/p>

#music<\/span><\/a> #design<\/span><\/a> #webdev<\/span><\/a> #beer<\/span><\/a><\/p>","url":"https:\/\/mastodon.social\/users\/dansup","avatar":"https:\/\/pixelfed.de\/storage\/avatars\/default.png?v=5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9","website":null,"local":false,"is_admin":false,"created_at":"2019-09-13T13:43:56.000000Z","header_bg":null}],"hashtags":[],"statuses":[]}""" - const val discover = """{"posts":[{"type":"photo:album","url":"https:\/\/pixelfed.de\/p\/MarkJR84\/147370947417083904","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/55d75a11bcc79dd8c98cf04e40ea0ea563508f35\/ujO0Zt8JuZLRgN6gP1JtVeIBcVxMQXDBtDAMDBUM_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/ieure\/142862071581773824","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/50fb80d927603c0a90afc2dc4a913d4b73d886e5\/1VaAV0uYoWGQKcJrM5i3F5EJeVriu6doEwLRPyyG_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/kindundkegel\/156147414724644864","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/5ac8092cc03257b7229f67c7c03f83c44e79aa61\/g5MM1vAF2q58vf9fDRVYfseDIC17AeB143HGPJMt_thumb.jpeg"},{"type":"photo:album","url":"https:\/\/pixelfed.de\/p\/die6095\/160363458813104128","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/66864b9ca55ad8d94df956d4042e7080c4767e0c\/IaUxEuNQveOL6BaA2f05KDaVexWwp4Ebr2z5j7P1_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/albert\/140256440240705536","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/832bb8b76564b4e4fdae78637ef71aec8dfc5413\/FPxvwun0CczGJ4vqS3pdPBP2V5nVwfYOdn6OZKwl_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/caffee\/133109637321986048","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/81f8bec749901cf1f57bc5ef516cd3bd837abd9c\/bf4WBOCjRXIfP38pEPVVlekNA7KAmOyagrdbNz9S_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/OBS\/160664565552648192","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d2fb84479480bba3f8caea6e286af5e68cc96043\/WEEyhhjYfDfUu4JQ0F5LWrrYM1CunD4U8TViqNa8_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/mybujo\/147730903064514560","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/af24fb17907442403d590308ee5728844f9a65fd\/6zaeKPo5GTDHCkRrl6SXSs288HJXcpvo3B0dK8La_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/xtz\/132236940895457280","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/f1114b52fd6acd5f7dd57f99f9c3f8b43bbe6755\/2y5IHLUv8QpGXPGu3OByFw43ixNTjMGr0SSM0IBc_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/miguel31416\/153835062528446464","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/3d238be60342b03e99597acb95f3202ff5e28ecc\/ndZt1Y9HP23vBDidfumz66ePyOtiDzx6PoVMUIh0_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/Unterholz\/149623530475098112","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/6718ff240b37ea60056b739cd9d723ff3f4a3182\/u9gU8532aqto5a5KyQbk4kyIvjiYlFYuydJfNCGW_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/eiseskaelte\/137934754728251392","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/6ef83b3e8ff8c33c534c9b6c5f6b9663ec95b7cc\/LtV219DZNDJfksAevobbrGTKPc3OBuA9PUBr6gXr_thumb.png"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/nimu\/144158481107259392","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/41a443fdae82d760bcbf858a3399cd421bc525eb\/oviSIwJ7bIHOxykZY7mUVl0xJ9Tj2abtZRPtCp9W_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/lastscater\/137926547892998144","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/90840e09c38be3ced54a83ea11f89956cb834b26\/HRCYdRziVXDsFw39LIHXrKReCs6YEFFTGnSMEb7e_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/vitorpires\/154891474964713472","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/4e45ce7f71c006f5284f4ffd39cde516fd852faa\/m4zRcA94lUtltjEUKIZtwMBEOQ74a3wMWpEqDM9S_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/cringe\/154235391417913344","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/924bac0d892d54a247213a5cd3e3f5aee4777fd1\/8l4TosvpojPCQbI2NDhaQTuvsfDwTHblRMp2Uf1f_thumb.png"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/lastscater\/137951787188621312","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/90840e09c38be3ced54a83ea11f89956cb834b26\/ds52GM0O7reAsvmGy2NSL2F1KPmNVqZJZTHEr7hI_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/LittleJoeMuc\/134359790792085504","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/c47c50c61d58c6392917640925d171e8b865fee0\/z6kBZxIDcSDkUwGOAGYckqZQc83om684paqYihdO_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/lastscater\/137925095220973568","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/90840e09c38be3ced54a83ea11f89956cb834b26\/QVry7No0BwNmF99FBCaBzFPsilFP4BNBkLvvugew_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/noooaaaiiin\/155404404713984000","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d4d5c08106389fc548c2aef0d8f3ff7cfa3d3069\/kU0W2rnm7mzqV7q7lo2uLluefiEzILzaYzRbgqgb_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/snaums\/145995392436277248","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/1f0d995ffb7a60c9475ed4e4c0ed25aaddfbde70\/DQf1Y1Dl11twi8cbPU2eKUVjuGk6H1nyPmULJsOz_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/Almusanz\/159960015694860288","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/040df869cbb7c9442d5462e2eb5e7ae825ba25d4\/Z0mSqgYI9faLSpYLgcoD7A0Jz1GDCMLDTQfcvNdu_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/eljay\/130011130146983936","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/ce7e035a402debf0fc3232ab682070d5ddc7b5f1\/e7n272XWbTKpYTwdHhJauPhUHWpYfrfEtjo7NPf7_thumb.jpeg"},{"type":"photo:album","url":"https:\/\/pixelfed.de\/p\/plukordinaire\/159325092709535744","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/fd35e6591b0dd4ecc10cd1d1c4c804e0fdeb250b\/dPs70MKL4UZuU2Nwt3nIIcHSp5WyCZQ8PNc3rRiV_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/pixelstreet\/147856947473944576","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/b8d1b7c62c7bf2412a8dcde4a75a9fa1d64a120a\/7K6iVwdF2L9Z7F2SHva9rwfxHiVV0Nrwg0YuPN82_thumb.png"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/Skoschi\/157826394389352448","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/17a88b10bc9476951217b4adfa8a60fa07df6689\/2EOZ9Of46bU4JVLJvfLavZL7qBUWndw3UyFV3ads_thumb.jpeg"},{"type":"photo:album","url":"https:\/\/pixelfed.de\/p\/paulakreuzer\/158959344812167168","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/4974892d3110a34dafe15a31cd6826370d90d4b6\/xeEnavra15D6FkG6Gm0ck64yYIQR4Y9CTdk2CcrJ_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/ArcanHell\/161515195255099392","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/2734fafccac3289550956b10b13bd2068fcd76be\/l1ewmXyd2dgrOVCXm0LRtM3MwPR7nd7RPj3eOehu_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/kindundkegel\/153475112908230656","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/5ac8092cc03257b7229f67c7c03f83c44e79aa61\/ODZil754pGMVBpiCCVHgxySCrBtb6umeENWjTJGO_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/Almusanz\/159953937712156672","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/040df869cbb7c9442d5462e2eb5e7ae825ba25d4\/KYtbVJ5Ms5OTxzL2tIKiuK9r75ZA7yKdxP9TgOyj_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/KC_Heine\/160060046221381632","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/29940ae2a397610159094385e0bb538f2710a1fe\/r7eUVmGLAYxzclAcmWNZyqudbwEhrgheEIyW3EBy_thumb.jpeg"},{"type":"photo:album","url":"https:\/\/pixelfed.de\/p\/ArcanHell\/161871256575152128","thumb":"https:\/\/pixelfed.de\/storage\/m\/d6eeff88aa0c9af7fd5f87cd6431a14d929d049b\/2734fafccac3289550956b10b13bd2068fcd76be\/mVX4GI2piQiLYKVj2g6zIUKDqw6AKwnFqJhH6uuJ_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/132029081461067776","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/HFzlgWRwwhxjQhUJsYYf7uWXSeb4RXhHlo0diZlN_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/ambratolm\/139802175395205120","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/27fcf21acabf918e928e1c7735b37c5ae31b0a7f\/02zt6B4hzfTRiTJ580V0NhBnqydCmzrkdvMuOD5F_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/Spaziergaenger\/161138718869164032","thumb":"https:\/\/pixelfed.de\/storage\/m\/d0931bf747b992a1c83e055753526516f2706111\/d75e944e8cc85b953d37d1a9d615f44d6e0cca22\/1ooyRfacILwBUX0iXb1cylr6Z6ZQxG2yc8sLHvSs_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/Hogan\/135357604443590656","thumb":"https:\/\/pixelfed.de\/storage\/m\/8fa1fd56cb1e6ebed8a4959ce89bd19543a4da5f\/c57656ac5a0c325a42f74916663cee33dca42475\/mqBjO74MM0mlFrRhtXKhg5X58hFaWGviqknnvXzq_thumb.jpeg"},{"type":"photo","url":"https:\/\/pixelfed.de\/p\/Mediokerl\/150314218074279936","thumb":"https:\/\/pixelfed.de\/storage\/m\/113a3e2124a33b1f5511e531953f5ee48456e0c7\/fbdb3b050807225b57b4b782c539bf7d2dd0ef23\/GAxpGSe1RKfQw7jSumE7jzetByfY2Kzer04UOgOU_thumb.jpeg"}]}""" - const val tokenJson = """{ - "access_token": "azerty", - "token_type": "Bearer", - "scope": "read write follow push", - "created_at": 12121212 - }""" - const val instanceJson = """{ - "uri": "REPLACEWITHDOMAIN", - "title": "PixelDroid", - "description": "Test server description.", - "email": "lejeu@epfl.ch", - "max_toot_chars": "666", - "thumbnail": "https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png", - "version": "69.420", - "registrations": true - }""" - var applicationJson = Gson().toJson(Application(name="PixelDroid", - website=null, vapid_key=null, client_id="286", - client_secret="2q3dHY29U8GNZ2eY6cbcw010cWk3qVGmWXxAJzn7")) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/testUtility/MockServer.kt b/app/src/androidTest/java/com/h/pixeldroid/testUtility/MockServer.kt deleted file mode 100644 index 2b6ca681..00000000 --- a/app/src/androidTest/java/com/h/pixeldroid/testUtility/MockServer.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.h.pixeldroid.testUtility - -import okhttp3.HttpUrl -import okhttp3.mockwebserver.Dispatcher -import okhttp3.mockwebserver.MockResponse -import okhttp3.mockwebserver.MockWebServer -import okhttp3.mockwebserver.RecordedRequest - - -class MockServer { - private val server = MockWebServer() - - companion object{ - private const val headerName = "Content-Type" - private const val headerValue = "application/json; charset=utf-8" - } - - fun start() { - try { - server.start(45106) - server.dispatcher = getDispatcher() - } catch (e: IllegalArgumentException) { - - } - } - fun stop(){ - try { - server.shutdown() - } catch (e: IllegalArgumentException) { - - } - } - - private fun getDispatcher(): Dispatcher { - return object : Dispatcher() { - @Throws(InterruptedException::class) - override fun dispatch(request: RecordedRequest): MockResponse { - when (request.path) { - "/api/v1/accounts/verify_credentials" -> return MockResponse() - .addHeader(headerName, headerValue) - .setResponseCode(200).setBody(JsonValues.accountJson) - "/api/v1/instance" -> return MockResponse() - .addHeader(headerName, headerValue) - .setResponseCode(200).setBody(JsonValues.instanceJson.replace("REPLACEWITHDOMAIN", getUrl().toString())) - "/api/v1/media" -> return MockResponse() - .addHeader(headerName, headerValue) - .setResponseCode(200).setBody(JsonValues.mediaUploadResponseJson) - "/api/v1/timelines/home" -> return MockResponse() - .addHeader(headerName, headerValue) - .setResponseCode(200).setBody(JsonValues.feedJson) - "/oauth/token" -> return MockResponse() - .addHeader(headerName, headerValue) - .setResponseCode(200).setBody(JsonValues.tokenJson) - } - when { - request.path?.contains("/api/v1/apps") == true -> { - return MockResponse() - .addHeader("Content-Type", "application/json; charset=utf-8") - .setResponseCode(200).setBody(JsonValues.applicationJson) - } - request.path?.contains("/api/v1/notifications") == true -> { - return MockResponse() - .addHeader("Content-Type", "application/json; charset=utf-8") - .setResponseCode(200).setBody(JsonValues.notificationsJson) - } - request.path?.contains("/api/v1/timelines/home") == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.feedJson) - } - request.path?.contains("/api/v1/timelines/public") == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.feedJson) - } - request.path?.contains("/api/v1/accounts/0/statuses") == true -> { - return MockResponse().setHttp2ErrorCode(401) - } - request.path?.matches("/api/v1/accounts/[0-9]*/statuses".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.accountStatusesJson) - } - request.path?.contains("/api/v1/statuses/0/context") == true -> { - return MockResponse().setHttp2ErrorCode(401) - } - request.path?.matches("/api/v1/statuses/[0-9]*/context".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.commentStatusesJson) - } - request.path?.contains("/api/v1/statuses/0/favourite") == true -> { - return MockResponse().setHttp2ErrorCode(401) - } - request.path?.matches("/api/v1/statuses/[0-9]*/favourite".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.likedJson) - } - request.path?.contains("/api/v1/statuses/0/unfavourite") == true -> { - return MockResponse().setHttp2ErrorCode(401) - } - request.path?.matches("/api/v1/statuses/[0-9]*/unfavourite".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.unlikeJson) - } - request.path?.contains("/api/v1/statuses") == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.unlikeJson) - } - request.path?.contains("/api/v1/accounts/0") == true -> { - return MockResponse().setHttp2ErrorCode(401) - } - request.path?.matches("/api/v1/accounts/[0-9]*".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.accountJson) - } - request.path?.contains("/api/v1/statuses/0/reblog") == true -> { - return MockResponse().setHttp2ErrorCode(401) - } - request.path?.matches("/api/v1/statuses/[0-9]*/reblog".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.reblogJson) - } - request.path?.contains("/api/v1/statuses/0/unreblog") == true -> { - return MockResponse().setHttp2ErrorCode(401) - } - request.path?.matches("/api/v1/statuses/[0-9]*/unreblog".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.unlikeJson) - } - request.path?.matches("/api/v1/accounts/[0-9]*/follow".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.followRelationshipJson) - } - request.path?.matches("/api/v1/accounts/[0-9]*/unfollow".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.unfollowRelationshipJson) - } - request.path?.contains("/api/v1/accounts/relationships") == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.relationshipJson) - } - request.path?.matches("/api/v1/accounts/[0-9]*/followers\\?limit=[0-9]*".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.followersJson) - } - request.path?.matches("/api/v1/accounts/[0-9]*/followers\\?since_id=[0-9]*&limit=[0-9]*".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.followersAfterJson) - } - request.path?.matches("/api/v1/accounts/[0-9]*/following\\?limit=[0-9]*".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.followersJson) - } - request.path?.matches("/api/v1/accounts/[0-9]*/following\\?since_id=[0-9]*&limit=[0-9]*".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.followersAfterJson) - } - request.path?.matches("/api/v2/search\\?type=hashtags&q=caturday&limit=[0-9]*&offset=[0-9]*".toRegex()) == true -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.searchEmpty) - } - request.path?.contains("/api/v2/search?type=hashtags&q=caturday")!!-> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.searchCaturdayHashtags) - } - request.path?.contains("/api/v2/search?type=statuses&q=caturday")!! -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.searchCaturday) - } - request.path?.contains("/api/v2/search?type=accounts&q=dansup")!! -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.searchDansupAccounts) - } - request.path?.matches("""/api/v2/search\?(max_id=[0-9]*&)?type=(accounts|statuses)&q=dansup(&limit=[0-9]*)?""".toRegex())!! -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.searchEmpty) - } - request.path?.contains("/api/v2/discover/posts")!! -> { - return MockResponse().addHeader( - "Content-Type", - "application/json; charset=utf-8" - ).setResponseCode(200).setBody(JsonValues.discover) - } - else -> return MockResponse().setResponseCode(404) - } - } - } - } - - fun getUrl(): HttpUrl { - return server.url("") - } - -} \ No newline at end of file diff --git a/app/src/androidTest/java/com/h/pixeldroid/testUtility/Values.kt b/app/src/androidTest/java/com/h/pixeldroid/testUtility/Values.kt new file mode 100644 index 00000000..c048b060 --- /dev/null +++ b/app/src/androidTest/java/com/h/pixeldroid/testUtility/Values.kt @@ -0,0 +1,28 @@ +package com.h.pixeldroid.testUtility + +import com.h.pixeldroid.BuildConfig.* +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity +import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity + +val testiTestoInstance = InstanceDatabaseEntity( + uri = INSTANCE_URI, + title = "PixelDroid CI instance", + maxStatusChars = 150, + maxPhotoSize = 64000, + maxVideoSize = 64000, + albumLimit = 4 +) +val testiTesto = UserDatabaseEntity( + + user_id = USER_ID, + instance_uri = INSTANCE_URI, + username = "testitesto", + display_name = "testi testo", + avatar_static = "$INSTANCE_URI/storage/avatars/default.jpg?v=0", + isActive = true, + accessToken = ACCESS_TOKEN, + refreshToken = REFRESH_TOKEN, + clientId = CLIENT_ID, + clientSecret = CLIENT_SECRET +) + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e8a79a8f..e02204f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,6 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" diff --git a/app/src/main/assets/licenses.html b/app/src/main/assets/licenses.html index c7947198..65f6dfb2 100644 --- a/app/src/main/assets/licenses.html +++ b/app/src/main/assets/licenses.html @@ -30883,6 +30883,253 @@

Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
+ +

dynamicanimation

+

Copyright © Google Inc. All rights reserved.

+

http://developer.android.com/tools/extras/support-library.html

+ +
+

+ Apache License +
+ Version 2.0, January 2004 +
+ http://www.apache.org/licenses/ +

+

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

+

1. Definitions.

+

+ "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. +

+

+ "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. +

+

+ "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. +

+

+ "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. +

+

+ "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. +

+

+ "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. +

+

+ "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). +

+

+ "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. +

+

+ "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." +

+

+ "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. +

+
+

2. Grant of Copyright License.

+

+ Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. +

+
+
+

3. Grant of Patent License.

+

+ Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. +

+
+
+

4. Redistribution.

+

+ You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: +

+
+
    +
  • + You must give any other recipients of the Work or + Derivative Works a copy of this License; and +
  • +
  • + You must cause any modified files to carry prominent notices + stating that You changed the files; and +
  • +
  • + You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of | + the Derivative Works; and +
  • +
  • + If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. +
  • +
+

+ You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. +

+
+

5. Submission of Contributions.

+

+ Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. +

+
+
+

6. Trademarks.

+

+ This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. +

+
+
+

7. Disclaimer of Warranty.

+

+ Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. +

+
+
+

8. Limitation of Liability.

+

+ In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. +

+
+
+

9. Accepting Warranty or Additional Liability.

+

+ While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. +

+
+

END OF TERMS AND CONDITIONS

+

APPENDIX: How to apply the Apache License to your work.

+

+ To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. +

+
Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
+
diff --git a/app/src/main/java/com/h/pixeldroid/LoginActivity.kt b/app/src/main/java/com/h/pixeldroid/LoginActivity.kt index a9ac3f63..fde97693 100644 --- a/app/src/main/java/com/h/pixeldroid/LoginActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/LoginActivity.kt @@ -10,22 +10,15 @@ import android.view.View import android.view.inputmethod.InputMethodManager import androidx.lifecycle.lifecycleScope import com.h.pixeldroid.databinding.ActivityLoginBinding -import com.h.pixeldroid.databinding.ActivityPostCreationBinding -import com.h.pixeldroid.utils.BaseActivity +import com.h.pixeldroid.utils.* import com.h.pixeldroid.utils.api.PixelfedAPI import com.h.pixeldroid.utils.api.objects.* import com.h.pixeldroid.utils.db.addUser import com.h.pixeldroid.utils.db.storeInstance -import com.h.pixeldroid.utils.hasInternet -import com.h.pixeldroid.utils.normalizeDomain -import com.h.pixeldroid.utils.openUrl -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import kotlinx.coroutines.supervisorScope -import okhttp3.HttpUrl +import kotlinx.coroutines.* import retrofit2.HttpException import java.io.IOException +import java.lang.IllegalArgumentException /** Overview of the flow of the login process: (boxes are requests done in parallel, @@ -104,9 +97,13 @@ class LoginActivity : BaseActivity() { private fun whatsAnInstance() { - val i = Intent(Intent.ACTION_VIEW) - i.data = Uri.parse("https://pixelfed.org/join") - startActivity(i) + val builder = AlertDialog.Builder(this) + builder.apply { + setView(layoutInflater.inflate(R.layout.whats_an_instance_explanation, null)) + setPositiveButton(android.R.string.ok) { _, _ -> } + } + // Create the AlertDialog + builder.show() } private fun hideKeyboard() { @@ -121,11 +118,7 @@ class LoginActivity : BaseActivity() { private fun registerAppToServer(normalizedDomain: String) { - try{ - HttpUrl.Builder().host(normalizedDomain.replace("https://", "")).scheme("https").build() - } catch (e: IllegalArgumentException) { - return failedRegistration(getString(R.string.invalid_domain)) - } + if(!validDomain(normalizedDomain)) return failedRegistration(getString(R.string.invalid_domain)) hideKeyboard() loadingAnimation(true) @@ -134,7 +127,6 @@ class LoginActivity : BaseActivity() { lifecycleScope.launch { try { - supervisorScope { } val credentialsDeferred: Deferred = async { try { pixelfedAPI.registerApplication( @@ -153,7 +145,6 @@ class LoginActivity : BaseActivity() { val clientId = credentials?.client_id ?: return@launch failedRegistration() preferences.edit() - .putString("domain", normalizedDomain) .putString("clientID", clientId) .putString("clientSecret", credentials.client_secret) .apply() @@ -177,18 +168,39 @@ class LoginActivity : BaseActivity() { normalizedDomain: String, clientId: String, nodeInfoSchemaUrl: String - ) { - val nodeInfo = try { + ) = coroutineScope { + + val nodeInfo: NodeInfo = try { pixelfedAPI.nodeInfoSchema(nodeInfoSchemaUrl) } catch (exception: IOException) { - return failedRegistration(getString(R.string.instance_error)) + return@coroutineScope failedRegistration(getString(R.string.instance_error)) } catch (exception: HttpException) { - return failedRegistration(getString(R.string.instance_error)) + return@coroutineScope failedRegistration(getString(R.string.instance_error)) } + val domain: String = try { + if (nodeInfo.hasInstanceEndpointInfo()) { + storeInstance(db, nodeInfo) + nodeInfo.metadata?.config?.site?.url + } else { + val instance: Instance = try { + pixelfedAPI.instance() + } catch (exception: IOException) { + return@coroutineScope failedRegistration(getString(R.string.instance_error)) + } catch (exception: HttpException) { + return@coroutineScope failedRegistration(getString(R.string.instance_error)) + } + storeInstance(db, nodeInfo = null, instance = instance) + instance.uri + } + } catch (e: IllegalArgumentException){ null } + ?: return@coroutineScope failedRegistration(getString(R.string.instance_error)) + + preferences.edit().putString("domain", normalizeDomain(domain)).apply() + + if (!nodeInfo.software?.name.orEmpty().contains("pixelfed")) { - val builder = AlertDialog.Builder(this@LoginActivity) - builder.apply { + AlertDialog.Builder(this@LoginActivity).apply { setMessage(R.string.instance_not_pixelfed_warning) setPositiveButton(R.string.instance_not_pixelfed_continue) { _, _ -> promptOAuth(normalizedDomain, clientId) @@ -197,13 +209,18 @@ class LoginActivity : BaseActivity() { loadingAnimation(false) wipeSharedSettings() } - } - // Create the AlertDialog - builder.show() + }.show() + } else if (nodeInfo.metadata?.config?.features?.mobile_apis != true) { + AlertDialog.Builder(this@LoginActivity).apply { + setMessage(R.string.api_not_enabled_dialog) + setNegativeButton(android.R.string.ok) { _, _ -> + loadingAnimation(false) + wipeSharedSettings() + } + }.show() } else { promptOAuth(normalizedDomain, clientId) } - } @@ -234,9 +251,6 @@ class LoginActivity : BaseActivity() { lifecycleScope.launch { try { - val instanceDeferred = async { - pixelfedAPI.instance() - } val token = pixelfedAPI.obtainToken( clientId, clientSecret, "$oauthScheme://$PACKAGE_ID", SCOPE, code, "authorization_code" @@ -244,20 +258,12 @@ class LoginActivity : BaseActivity() { if (token.access_token == null) { return@launch failedRegistration(getString(R.string.token_error)) } - - val instance = instanceDeferred.await() - - if (instance.uri == null) { - return@launch failedRegistration(getString(R.string.instance_error)) - } - - storeInstance(db, instance) storeUser( token.access_token, token.refresh_token, clientId, clientSecret, - instance.uri + domain ) wipeSharedSettings() } catch (exception: IOException) { diff --git a/app/src/main/java/com/h/pixeldroid/MainActivity.kt b/app/src/main/java/com/h/pixeldroid/MainActivity.kt index fbfc9b2a..7bd05c53 100644 --- a/app/src/main/java/com/h/pixeldroid/MainActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/MainActivity.kt @@ -61,15 +61,16 @@ class MainActivity : BaseActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - TraceDroidEmailSender.sendStackTraces("contact@pixeldroid.org", this) - //get the currently active user user = db.userDao().getActiveUser() //Check if we have logged in and gotten an access token if (user == null) { launchActivity(LoginActivity(), firstTime = true) + finish() } else { + TraceDroidEmailSender.sendStackTraces("contact@pixeldroid.org", this) + setupDrawer() val tabs: List<() -> Fragment> = listOf( diff --git a/app/src/main/java/com/h/pixeldroid/postCreation/PostCreationActivity.kt b/app/src/main/java/com/h/pixeldroid/postCreation/PostCreationActivity.kt index 11839d03..26267464 100644 --- a/app/src/main/java/com/h/pixeldroid/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/postCreation/PostCreationActivity.kt @@ -1,10 +1,8 @@ package com.h.pixeldroid.postCreation import android.app.Activity -import android.content.ContentResolver -import android.content.ContentValues -import android.content.Context -import android.content.Intent +import android.app.AlertDialog +import android.content.* import android.media.MediaScannerConnection import android.net.Uri import android.os.Build @@ -16,26 +14,23 @@ import android.util.Log import android.view.View import android.view.View.INVISIBLE import android.view.View.VISIBLE -import android.widget.Button -import android.widget.ImageButton import android.widget.Toast import androidx.core.net.toFile import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar -import com.google.android.material.textfield.TextInputLayout -import com.h.pixeldroid.utils.BaseActivity import com.h.pixeldroid.MainActivity import com.h.pixeldroid.R import com.h.pixeldroid.databinding.ActivityPostCreationBinding -import com.h.pixeldroid.utils.api.PixelfedAPI import com.h.pixeldroid.postCreation.camera.CameraActivity import com.h.pixeldroid.postCreation.carousel.CarouselItem import com.h.pixeldroid.postCreation.carousel.ImageCarousel -import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity -import com.h.pixeldroid.utils.api.objects.Attachment -import com.h.pixeldroid.utils.api.objects.Instance import com.h.pixeldroid.postCreation.photoEdit.PhotoEditActivity +import com.h.pixeldroid.utils.BaseActivity +import com.h.pixeldroid.utils.api.PixelfedAPI +import com.h.pixeldroid.utils.api.objects.Attachment +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity +import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers @@ -47,14 +42,18 @@ import java.io.OutputStream import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList +import kotlin.math.ceil +import kotlin.properties.Delegates private const val TAG = "Post Creation Activity" private const val MORE_PICTURES_REQUEST_CODE = 0xffff data class PhotoData( var imageUri: Uri, + var size: Long, var uploadId: String? = null, - var progress: Int? = null + var progress: Int? = null, + var imageDescription: String? = null, ) class PostCreationActivity : BaseActivity() { @@ -64,6 +63,7 @@ class PostCreationActivity : BaseActivity() { private var positionResult = 0 private var user: UserDatabaseEntity? = null + private lateinit var instance: InstanceDatabaseEntity private val photoData: ArrayList = ArrayList() @@ -74,48 +74,40 @@ class PostCreationActivity : BaseActivity() { binding = ActivityPostCreationBinding.inflate(layoutInflater) setContentView(binding.root) - // get image URIs - if(intent.clipData != null) { - val count = intent.clipData!!.itemCount - for (i in 0 until count) { - intent.clipData!!.getItemAt(i).uri.let { - photoData.add(PhotoData(it)) - } - } - } - user = db.userDao().getActiveUser() - val instances = db.instanceDao().getAll() + instance = user?.run { + db.instanceDao().getAll().first { instanceDatabaseEntity -> + instanceDatabaseEntity.uri.contains(instance_uri) + } + } ?: InstanceDatabaseEntity("", "") - binding.postTextInputLayout.counterMaxLength = if (user != null){ - val thisInstances = - instances.filter { instanceDatabaseEntity -> - instanceDatabaseEntity.uri.contains(user!!.instance_uri) - } - thisInstances.first().max_toot_chars - } else { - Instance.DEFAULT_MAX_TOOT_CHARS - } + binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars + + // get image URIs + intent.clipData?.let { addPossibleImages(it) } accessToken = user?.accessToken.orEmpty() pixelfedAPI = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db) val carousel: ImageCarousel = binding.carousel - carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) }) + carousel.addData(photoData.map { CarouselItem(it.imageUri) }) carousel.layoutCarouselCallback = { - //TODO transition instead of at once if(it){ // Became a carousel - binding.toolbar3.visibility = VISIBLE + binding.toolbarPostCreation.visibility = VISIBLE } else { // Became a grid - binding.toolbar3.visibility = INVISIBLE + binding.toolbarPostCreation.visibility = INVISIBLE } } + carousel.maxEntries = instance.albumLimit carousel.addPhotoButtonCallback = { addPhoto(applicationContext) } + carousel.updateDescriptionCallback = { position: Int, description: String -> + photoData[position].imageDescription = description + } // get the description and send the post binding.postCreationSendButton.setOnClickListener { @@ -152,7 +144,64 @@ class PostCreationActivity : BaseActivity() { binding.removePhotoButton.setOnClickListener { carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition -> photoData.removeAt(currentPosition) - carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) }) + carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) }) + binding.addPhotoButton.isEnabled = true + } + } + } + + /** + * Will add as many images as possible to [photoData], from the [clipData], and if + * ([photoData].size + [clipData].itemCount) > [albumLimit] then it will only add as many images + * as are legal (if any) and a dialog will be shown to the user alerting them of this fact. + */ + private fun addPossibleImages(clipData: ClipData){ + var count = clipData.itemCount + if(count + photoData.size > instance.albumLimit){ + AlertDialog.Builder(this).apply { + setMessage(getString(R.string.total_exceeds_album_limit).format(instance.albumLimit)) + setNegativeButton(android.R.string.ok) { _, _ -> } + }.show() + count = count.coerceAtMost(instance.albumLimit - photoData.size) + } + if (count + photoData.size >= instance.albumLimit) { + // Disable buttons to add more images + binding.addPhotoButton.isEnabled = false + } + for (i in 0 until count) { + clipData.getItemAt(i).uri.let { + val size: Long = + if (it.toString().startsWith("content")) { + contentResolver.query(it, null, null, null, null) + ?.use { cursor -> + /* Get the column indexes of the data in the Cursor, + * move to the first row in the Cursor, get the data, + * and display it. + */ + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + cursor.getLong(sizeIndex) + } ?: 0 + } else { + it.toFile().length() + } + val sizeInkBytes = ceil(size.toDouble() / 1000).toLong() + if(sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize){ + val maxSize = when { + instance.maxPhotoSize != instance.maxVideoSize -> { + val type = contentResolver.getType(it) + if(type?.startsWith("video/") == true){ + instance.maxVideoSize + } else instance.maxPhotoSize + } + else -> instance.maxPhotoSize + } + AlertDialog.Builder(this).apply { + setMessage(getString(R.string.size_exceeds_instance_limit).format(photoData.size + 1, sizeInkBytes, maxSize)) + setNegativeButton(android.R.string.ok) { _, _ -> } + }.show() + } + photoData.add(PhotoData(imageUri = it, size = size)) } } } @@ -177,21 +226,21 @@ class PostCreationActivity : BaseActivity() { if(path.startsWith("file")) { MediaScannerConnection.scanFile( - this, - arrayOf(path.toUri().toFile().absolutePath), - null + this, + arrayOf(path.toUri().toFile().absolutePath), + null ) { path, uri -> if (uri == null) { Log.e( - "NEW IMAGE SCAN FAILED", - "Tried to scan $path, but it failed" + "NEW IMAGE SCAN FAILED", + "Tried to scan $path, but it failed" ) } } } Snackbar.make( - button, getString(R.string.save_image_success), - Snackbar.LENGTH_LONG + button, getString(R.string.save_image_success), + Snackbar.LENGTH_LONG ).show() } @@ -204,8 +253,8 @@ class PostCreationActivity : BaseActivity() { contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name) contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png") contentValues.put( - MediaStore.MediaColumns.RELATIVE_PATH, - Environment.DIRECTORY_PICTURES + MediaStore.MediaColumns.RELATIVE_PATH, + Environment.DIRECTORY_PICTURES ) val imageUri: Uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!! @@ -228,7 +277,7 @@ class PostCreationActivity : BaseActivity() { val content = editText?.length() ?: 0 if (content > counterMaxLength) { // error, too many characters - error = getString(R.string.description_max_characters).format(counterMaxLength) + error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength) return false } } @@ -252,23 +301,7 @@ class PostCreationActivity : BaseActivity() { val imageUri = data.imageUri val imageInputStream = contentResolver.openInputStream(imageUri)!! - val size = - if (imageUri.toString().startsWith("content")) { - contentResolver.query(imageUri, null, null, null, null) - ?.use { cursor -> - /* Get the column indexes of the data in the Cursor, - * move to the first row in the Cursor, get the data, - * and display it. - */ - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - cursor.moveToFirst() - cursor.getLong(sizeIndex) - } ?: 0 - } else { - imageUri.toFile().length() - } - - val imagePart = ProgressRequestBody(imageInputStream, size) + val imagePart = ProgressRequestBody(imageInputStream, data.size) val requestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", System.currentTimeMillis().toString(), imagePart) @@ -283,32 +316,43 @@ class PostCreationActivity : BaseActivity() { } var postSub: Disposable? = null - val inter = pixelfedAPI.mediaUpload("Bearer $accessToken", requestBody.parts[0]) + + val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) } + + + val inter = pixelfedAPI.mediaUpload("Bearer $accessToken", description, requestBody.parts[0]) postSub = inter .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { attachment: Attachment -> - data.progress = 0 - data.uploadId = attachment.id!! - }, - { e -> - binding.uploadError.visibility = View.VISIBLE - e.printStackTrace() - postSub?.dispose() - sub.dispose() - }, - { - data.progress = 100 - if(photoData.all{it.progress == 100}){ - binding.uploadProgressBar.visibility = View.GONE - binding.uploadCompletedTextview.visibility = View.VISIBLE - post() + { attachment: Attachment -> + data.progress = 0 + data.uploadId = attachment.id!! + }, + { e: Throwable -> + binding.uploadError.visibility = View.VISIBLE + if(e is HttpException){ + binding.uploadErrorTextExplanation.text = + getString(R.string.upload_error).format(e.code()) + binding.uploadErrorTextExplanation.visibility= VISIBLE + } else { + binding.uploadErrorTextExplanation.visibility= View.GONE + } + e.printStackTrace() + postSub?.dispose() + sub.dispose() + }, + { + data.progress = 100 + if (photoData.all { it.progress == 100 && it.uploadId != null }) { + binding.uploadProgressBar.visibility = View.GONE + binding.uploadCompletedTextview.visibility = View.VISIBLE + post() + } + postSub?.dispose() + sub.dispose() } - postSub?.dispose() - sub.dispose() - } ) } } @@ -319,23 +363,23 @@ class PostCreationActivity : BaseActivity() { lifecycleScope.launchWhenCreated { try { pixelfedAPI.postStatus( - authorization = "Bearer $accessToken", - statusText = description, - media_ids = photoData.mapNotNull { it.uploadId }.toList() + authorization = "Bearer $accessToken", + statusText = description, + media_ids = photoData.mapNotNull { it.uploadId }.toList() ) - Toast.makeText(applicationContext,getString(R.string.upload_post_success), - Toast.LENGTH_SHORT).show() + Toast.makeText(applicationContext, getString(R.string.upload_post_success), + Toast.LENGTH_SHORT).show() val intent = Intent(this@PostCreationActivity, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivity(intent) } catch (exception: IOException) { - Toast.makeText(applicationContext,getString(R.string.upload_post_error), - Toast.LENGTH_SHORT).show() + Toast.makeText(applicationContext, getString(R.string.upload_post_error), + Toast.LENGTH_SHORT).show() Log.e(TAG, exception.toString()) enableButton(true) } catch (exception: HttpException) { - Toast.makeText(applicationContext,getString(R.string.upload_post_failed), - Toast.LENGTH_SHORT).show() + Toast.makeText(applicationContext, getString(R.string.upload_post_failed), + Toast.LENGTH_SHORT).show() Log.e(TAG, exception.response().toString() + exception.message().toString()) enableButton(true) } @@ -369,7 +413,7 @@ class PostCreationActivity : BaseActivity() { if (resultCode == Activity.RESULT_OK && data != null) { photoData[positionResult].imageUri = data.getStringExtra("result")!!.toUri() - binding.carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) }) + binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) }) photoData[positionResult].progress = null photoData[positionResult].uploadId = null @@ -377,15 +421,13 @@ class PostCreationActivity : BaseActivity() { Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() } } else if (requestCode == MORE_PICTURES_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK && data?.clipData != null) { - val count = data.clipData!!.itemCount - for (i in 0 until count) { - val imageUri: Uri = data.clipData!!.getItemAt(i).uri - photoData.add(PhotoData(imageUri)) + if (resultCode == Activity.RESULT_OK && data?.clipData != null) { + data.clipData?.let { + addPossibleImages(it) } - binding.carousel.addData(photoData.map { CarouselItem(it.imageUri.toString()) }) + binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) }) } else if(resultCode != Activity.RESULT_CANCELED){ Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselAdapter.kt b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselAdapter.kt index 1062abbb..759343fc 100644 --- a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselAdapter.kt +++ b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselAdapter.kt @@ -13,18 +13,17 @@ import com.h.pixeldroid.R class CarouselAdapter( - @LayoutRes private val itemLayout: Int, - @IdRes private val imageViewId: Int, - var listener: OnItemClickListener? = null, - private val imageScaleType: ImageView.ScaleType, - private val imagePlaceholder: Drawable?, - private val carousel: Boolean + @LayoutRes private val itemLayout: Int, + @IdRes private val imageViewId: Int, + var listener: OnItemClickListener? = null, + private val imageScaleType: ImageView.ScaleType, + private val imagePlaceholder: Drawable?, + private val carousel: Boolean, + var maxEntries: Int?, ) : RecyclerView.Adapter() { private val dataList: MutableList = mutableListOf() - - class MyViewHolder(itemView: View, imageViewId: Int) : RecyclerView.ViewHolder(itemView) { var img: ImageView = itemView.findViewById(imageViewId) } @@ -51,6 +50,7 @@ class CarouselAdapter( override fun getItemCount(): Int { return if(carousel) dataList.size + else if (maxEntries != null && dataList.size >= maxEntries!!) maxEntries!! else dataList.size + 1 } @@ -95,6 +95,11 @@ class CarouselAdapter( } } + fun updateDescription(position: Int, description: String) { + dataList[position] = dataList[position].copy(caption = description) + notifyItemChanged(position) + } + fun addAll(dataList: List) { this.dataList.clear() diff --git a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselItem.kt b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselItem.kt index a50326c0..8b3ef70e 100644 --- a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselItem.kt +++ b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/CarouselItem.kt @@ -1,8 +1,10 @@ package com.h.pixeldroid.postCreation.carousel +import android.net.Uri + data class CarouselItem constructor( - val imageUrl: String? = null, - val caption: String? = null + val imageUrl: Uri, + val caption: String? = null ) { - constructor(imageUrl: String? = null) : this(imageUrl, null) + constructor(imageUrl: Uri) : this(imageUrl, null) } \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/ImageCarousel.kt b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/ImageCarousel.kt index 3185dc89..88a7913b 100644 --- a/app/src/main/java/com/h/pixeldroid/postCreation/carousel/ImageCarousel.kt +++ b/app/src/main/java/com/h/pixeldroid/postCreation/carousel/ImageCarousel.kt @@ -8,10 +8,7 @@ import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater import android.view.View -import android.widget.FrameLayout -import android.widget.ImageButton -import android.widget.ImageView -import android.widget.TextView +import android.widget.* import androidx.annotation.Dimension import androidx.annotation.IdRes import androidx.annotation.LayoutRes @@ -19,6 +16,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.recyclerview.widget.* import com.h.pixeldroid.R +import com.h.pixeldroid.databinding.ImageCarouselBinding import me.relex.circleindicator.CircleIndicator2 import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.Nullable @@ -31,6 +29,8 @@ class ImageCarousel( private var adapter: CarouselAdapter? = null + private lateinit var binding: ImageCarouselBinding + private val scaleTypeArray = arrayOf( ImageView.ScaleType.MATRIX, ImageView.ScaleType.FIT_XY, @@ -42,11 +42,9 @@ class ImageCarousel( ImageView.ScaleType.CENTER_INSIDE ) - private lateinit var carouselView: View private lateinit var recyclerView: RecyclerView private lateinit var tvCaption: TextView - private lateinit var previousButtonContainer: FrameLayout - private lateinit var nextButtonContainer: FrameLayout + private lateinit var editTextMediaDescription: EditText private var snapHelper: SnapHelper = PagerSnapHelper() var indicator: CircleIndicator2? = null @@ -151,9 +149,9 @@ class ImageCarousel( set(value) { field = value - previousButtonContainer.visibility = + binding.btnPrevious.visibility = if (showNavigationButtons) View.VISIBLE else View.GONE - nextButtonContainer.visibility = + binding.btnNext.visibility = if (showNavigationButtons) View.VISIBLE else View.GONE } @@ -194,69 +192,19 @@ class ImageCarousel( initAdapter() } - @LayoutRes - var previousButtonLayout: Int = R.layout.previous_button_layout - set(value) { - field = value - - btnPrevious = null - - previousButtonContainer.removeAllViews() - LayoutInflater.from(context).apply { - inflate(previousButtonLayout, previousButtonContainer, true) - } - } - - @IdRes - var previousButtonId: Int = R.id.btn_next - set(value) { - field = value - - btnPrevious = carouselView.findViewById(previousButtonId) - - btnPrevious?.setOnClickListener { - previous() - } - } - @Dimension(unit = Dimension.PX) var previousButtonMargin: Int = 0 set(value) { field = value - val previousButtonParams = previousButtonContainer.layoutParams as LayoutParams + val previousButtonParams = binding.btnPrevious.layoutParams as LayoutParams previousButtonParams.setMargins( previousButtonMargin, 0, 0, 0 ) - previousButtonContainer.layoutParams = previousButtonParams - } - - @LayoutRes - var nextButtonLayout: Int = R.layout.next_button_layout - set(value) { - field = value - - btnNext = null - - nextButtonContainer.removeAllViews() - LayoutInflater.from(context).apply { - inflate(nextButtonLayout, nextButtonContainer, true) - } - } - - @IdRes - var nextButtonId: Int = R.id.btn_previous - set(value) { - field = value - - btnNext = carouselView.findViewById(nextButtonId) - - btnNext?.setOnClickListener { - next() - } + binding.btnPrevious.layoutParams = previousButtonParams } @Dimension(unit = Dimension.PX) @@ -264,22 +212,22 @@ class ImageCarousel( set(value) { field = value - val nextButtonParams = nextButtonContainer.layoutParams as LayoutParams + val nextButtonParams = binding.btnNext.layoutParams as LayoutParams nextButtonParams.setMargins( 0, 0, nextButtonMargin, 0 ) - nextButtonContainer.layoutParams = nextButtonParams + binding.btnNext.layoutParams = nextButtonParams } var showLayoutSwitchButton: Boolean = true set(value) { field = value - btnGrid = findViewById(R.id.switchToGridButton) - btnCarousel = findViewById(R.id.switchToCarouselButton) + btnGrid = binding.switchToGridButton + btnCarousel = binding.switchToCarouselButton btnGrid?.setOnClickListener { layoutCarousel = false @@ -304,6 +252,9 @@ class ImageCarousel( var layoutCarouselCallback: ((Boolean) -> Unit)? = null + var updateDescriptionCallback: ((position: Int, description: String) -> Unit)? = null + + var layoutCarousel: Boolean = true set(value) { field = value @@ -313,10 +264,16 @@ class ImageCarousel( btnNext?.visibility = VISIBLE btnPrevious?.visibility = VISIBLE + + binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE + tvCaption.visibility = if(editingMediaDescription) INVISIBLE else VISIBLE } else { recyclerView.layoutManager = GridLayoutManager(context, 3) btnNext?.visibility = GONE btnPrevious?.visibility = GONE + + binding.editMediaDescriptionLayout.visibility = INVISIBLE + tvCaption.visibility = INVISIBLE } showIndicator = value @@ -330,6 +287,43 @@ class ImageCarousel( var addPhotoButtonCallback: (() -> Unit)? = null + var editingMediaDescription: Boolean = false + set(value){ + + if(layoutCarousel){ + field = value + if(value) editTextMediaDescription.setText(currentDescription) + else { + val description = editTextMediaDescription.text.toString() + currentDescription = description + adapter?.updateDescription(currentPosition, description) + updateDescriptionCallback?.invoke(currentPosition, description) + } + binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE + tvCaption.visibility = if(value) INVISIBLE else VISIBLE + + } + + } + + var currentDescription: String? = null + set(value) { + if(!value.isNullOrEmpty()) { + field = value + tvCaption.text = value + } else { + field = null + tvCaption.text = context.getText(R.string.no_media_description) + } + + } + + var maxEntries: Int? = null + set(value){ + field = value + adapter?.maxEntries = value + } + init { @@ -341,12 +335,11 @@ class ImageCarousel( private fun initViews() { - carouselView = LayoutInflater.from(context).inflate(R.layout.image_carousel, this) + binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this, true) - recyclerView = carouselView.findViewById(R.id.recyclerView) - tvCaption = carouselView.findViewById(R.id.tv_caption) - previousButtonContainer = carouselView.findViewById(R.id.previous_button_container) - nextButtonContainer = carouselView.findViewById(R.id.next_button_container) + recyclerView = binding.recyclerView + tvCaption = binding.tvCaption + editTextMediaDescription = binding.editTextMediaDescription recyclerView.setHasFixedSize(true) @@ -403,16 +396,8 @@ class ImageCarousel( R.id.img ) - previousButtonLayout = R.layout.previous_button_layout - - previousButtonId = R.id.btn_previous - previousButtonMargin = 4.dpToPx(context) - nextButtonLayout = R.layout.next_button_layout - - nextButtonId = R.id.btn_next - nextButtonMargin = 4.dpToPx(context) showNavigationButtons = getBoolean( @@ -440,12 +425,13 @@ class ImageCarousel( private fun initAdapter() { adapter = CarouselAdapter( - itemLayout = itemLayout, - imageViewId = imageViewId, - listener = onItemClickListener, - imageScaleType = imageScaleType, - imagePlaceholder = imagePlaceholder, - carousel = layoutCarousel + itemLayout = itemLayout, + imageViewId = imageViewId, + listener = onItemClickListener, + imageScaleType = imageScaleType, + imagePlaceholder = imagePlaceholder, + carousel = layoutCarousel, + maxEntries = maxEntries ) recyclerView.adapter = adapter @@ -474,7 +460,13 @@ class ImageCarousel( val dataItem = adapter?.getItem(position) dataItem?.apply { - tvCaption.text = this.caption + caption.apply { + if(layoutCarousel){ + binding.editMediaDescriptionLayout.visibility = INVISIBLE + tvCaption.visibility = VISIBLE + } + currentDescription = this + } } } } @@ -499,12 +491,27 @@ class ImageCarousel( } }) + + tvCaption.setOnClickListener { + editingMediaDescription = true + } + + binding.btnNext.setOnClickListener { + next() + } + + binding.btnPrevious.setOnClickListener { + previous() + } + binding.imageDescriptionButton.setOnClickListener { + editingMediaDescription = false + } } private fun initIndicator() { // If no custom indicator added, then default indicator will be shown. if (indicator == null) { - indicator = carouselView.findViewById(R.id.indicator) + indicator = binding.indicator isBuiltInIndicator = true } diff --git a/app/src/main/java/com/h/pixeldroid/postCreation/photoEdit/PhotoEditActivity.kt b/app/src/main/java/com/h/pixeldroid/postCreation/photoEdit/PhotoEditActivity.kt index c42c9971..058aede1 100644 --- a/app/src/main/java/com/h/pixeldroid/postCreation/photoEdit/PhotoEditActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/postCreation/photoEdit/PhotoEditActivity.kt @@ -49,7 +49,7 @@ private val REQUIRED_PERMISSIONS = arrayOf( class PhotoEditActivity : BaseActivity() { - private var saving: Boolean = false + var saving: Boolean = false private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 private val BRIGHTNESS_START = 0 private val SATURATION_START = 1.0f @@ -319,6 +319,7 @@ class PhotoEditActivity : BaseActivity() { permissions: Array, grantResults: IntArray ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) if(grantResults.size > 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) { diff --git a/app/src/main/java/com/h/pixeldroid/posts/NestedScrollableHost.kt b/app/src/main/java/com/h/pixeldroid/posts/NestedScrollableHost.kt new file mode 100644 index 00000000..2a99aa95 --- /dev/null +++ b/app/src/main/java/com/h/pixeldroid/posts/NestedScrollableHost.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.h.pixeldroid.posts + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.viewpager2.widget.ViewPager2 +import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL +import kotlin.math.absoluteValue +import kotlin.math.sign + +/** + * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem + * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as + * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout. + * + * This solution has limitations when using multiple levels of nested scrollable elements + * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). + */ +class NestedScrollableHost : ConstraintLayout { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + private var touchSlop = 0 + private var initialX = 0f + private var initialY = 0f + private val parentViewPager: ViewPager2? + get() { + var v: View? = parent as? View + while (v != null && v !is ViewPager2) { + v = v.parent as? View + } + return v as? ViewPager2 + } + + private val child: View? get() = if (childCount > 0) getChildAt(0) else null + + init { + touchSlop = ViewConfiguration.get(context).scaledTouchSlop + } + + private fun canChildScroll(orientation: Int, delta: Float): Boolean { + val direction = -delta.sign.toInt() + return when (orientation) { + 0 -> child?.canScrollHorizontally(direction) ?: false + 1 -> child?.canScrollVertically(direction) ?: false + else -> throw IllegalArgumentException() + } + } + + override fun onInterceptTouchEvent(e: MotionEvent): Boolean { + handleInterceptTouchEvent(e) + return super.onInterceptTouchEvent(e) + } + + private fun handleInterceptTouchEvent(e: MotionEvent) { + val orientation = parentViewPager?.orientation ?: return + + // Early return if child can't scroll in same direction as parent + if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) { + return + } + + if (e.action == MotionEvent.ACTION_DOWN) { + initialX = e.x + initialY = e.y + parent.requestDisallowInterceptTouchEvent(true) + } else if (e.action == MotionEvent.ACTION_MOVE) { + val dx = e.x - initialX + val dy = e.y - initialY + val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL + + // assuming ViewPager2 touch-slop is 2x touch-slop of child + val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f + val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f + + if (scaledDx > touchSlop || scaledDy > touchSlop) { + if (isVpHorizontal == (scaledDy > scaledDx)) { + // Gesture is perpendicular, allow all parents to intercept + parent.requestDisallowInterceptTouchEvent(false) + } else { + // Gesture is parallel, query child if movement in that direction is possible + if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) { + // Child can scroll, disallow all parents to intercept + parent.requestDisallowInterceptTouchEvent(true) + } else { + // Child cannot scroll, allow all parents to intercept + parent.requestDisallowInterceptTouchEvent(false) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/posts/PostActivity.kt b/app/src/main/java/com/h/pixeldroid/posts/PostActivity.kt index 0cb7bcaa..be453563 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/PostActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/PostActivity.kt @@ -1,27 +1,36 @@ package com.h.pixeldroid.posts +import android.content.Context import android.os.Bundle import android.util.Log +import android.view.LayoutInflater import android.view.View +import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE +import android.widget.LinearLayout +import android.widget.Toast import androidx.lifecycle.lifecycleScope import com.h.pixeldroid.R import com.h.pixeldroid.databinding.ActivityPostBinding -import com.h.pixeldroid.utils.api.objects.DiscoverPost -import com.h.pixeldroid.utils.api.objects.Status -import com.h.pixeldroid.utils.api.objects.Status.Companion.DISCOVER_TAG -import com.h.pixeldroid.utils.api.objects.Status.Companion.DOMAIN_TAG -import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG +import com.h.pixeldroid.databinding.CommentBinding import com.h.pixeldroid.utils.BaseActivity +import com.h.pixeldroid.utils.api.PixelfedAPI +import com.h.pixeldroid.utils.api.objects.Mention +import com.h.pixeldroid.utils.api.objects.Status +import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_COMMENT_TAG +import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG +import com.h.pixeldroid.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG +import com.h.pixeldroid.utils.displayDimensionsInPx import retrofit2.HttpException import java.io.IOException class PostActivity : BaseActivity() { - private lateinit var postFragment : PostFragment lateinit var domain : String private lateinit var accessToken : String private lateinit var binding: ActivityPostBinding + private lateinit var status: Status + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityPostBinding.inflate(layoutInflater) @@ -29,23 +38,40 @@ class PostActivity : BaseActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) - val status = intent.getSerializableExtra(POST_TAG) as Status? - val discoverPost: DiscoverPost? = intent.getSerializableExtra(DISCOVER_TAG) as DiscoverPost? + status = intent.getSerializableExtra(POST_TAG) as Status + val viewComments: Boolean = (intent.getSerializableExtra(VIEW_COMMENTS_TAG) ?: false) as Boolean + val postComment: Boolean = (intent.getSerializableExtra(POST_COMMENT_TAG) ?: false) as Boolean val user = db.userDao().getActiveUser() domain = user?.instance_uri.orEmpty() accessToken = user?.accessToken.orEmpty() - postFragment = PostFragment() - val arguments = Bundle() - arguments.putString(DOMAIN_TAG, domain) - if (discoverPost != null) { - binding.postProgressBar.visibility = View.VISIBLE - getDiscoverPost(arguments, discoverPost) - } else { - initializeFragment(arguments, status) + supportActionBar?.title = getString(R.string.post_title).format(status.account?.getDisplayName()) + + val holder = StatusViewHolder(binding.postFragmentSingle) + + holder.bind(status, apiHolder.api!!, db, lifecycleScope, displayDimensionsInPx(), isActivity = true) + + val credential = "Bearer $accessToken" + activateCommenter(credential) + + if(viewComments || postComment){ + //Scroll already down as much as possible (since comments are not loaded yet) + binding.scrollview.requestChildFocus(binding.editComment, binding.editComment) + + //Open keyboard if we want to post a comment + if(postComment && binding.editComment.requestFocus()) { + window.setSoftInputMode(SOFT_INPUT_STATE_VISIBLE) + binding.editComment.requestFocus() + } + + // also retrieve comments if we're not posting the comment + if(!postComment) retrieveComments(apiHolder.api!!, credential) + } + binding.postFragmentSingle.viewComments.setOnClickListener { + retrieveComments(apiHolder.api!!, credential) } } @@ -54,32 +80,109 @@ class PostActivity : BaseActivity() { return true } - private fun getDiscoverPost( - arguments: Bundle, - discoverPost: DiscoverPost - ) { - val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db) - val id = discoverPost.url?.substringAfterLast('/') ?: "" - lifecycleScope.launchWhenCreated { - try { - val status = api.getStatus("Bearer $accessToken", id) - binding.postProgressBar.visibility = View.GONE - initializeFragment(arguments, status) - } catch (exception: IOException) { - //TODO show error message - Log.e("PostActivity:", exception.toString()) - } catch (exception: HttpException) { + private fun activateCommenter(credential: String) { + //Activate commenter + binding.submitComment.setOnClickListener { + val textIn = binding.editComment.text + //Open text input + if(textIn.isNullOrEmpty()) { + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.empty_comment), + Toast.LENGTH_SHORT + ).show() + } else { + //Post the comment + lifecycleScope.launchWhenCreated { + apiHolder.api?.let { it1 -> postComment(it1, credential) } + } } } } - private fun initializeFragment(arguments: Bundle, status: Status?){ - supportActionBar?.title = getString(R.string.post_title).format(status!!.account?.getDisplayName()) - arguments.putSerializable(POST_TAG, status) - postFragment.arguments = arguments - supportFragmentManager.isStateSaved - supportFragmentManager.beginTransaction() - .add(R.id.postFragmentSingle, postFragment).commit() - binding.postFragmentSingle.visibility = View.VISIBLE + private fun addComment(context: Context, commentContainer: LinearLayout, + commentUsername: String, commentContent: String, mentions: List, + credential: String) { + + + val itemBinding = CommentBinding.inflate( + LayoutInflater.from(context), commentContainer, true + ) + + itemBinding.user.text = commentUsername + itemBinding.commentText.text = parseHTMLText( + commentContent, + mentions, + apiHolder.api!!, + context, + credential, + lifecycleScope + ) } + + private fun retrieveComments(api: PixelfedAPI, credential: String) { + lifecycleScope.launchWhenCreated { + status.id.let { + try { + val statuses = api.statusComments(it, credential).descendants + + binding.commentContainer.removeAllViews() + + //Create the new views for each comment + for (status in statuses) { + addComment(binding.root.context, binding.commentContainer, status.account!!.username!!, + status.content!!, status.mentions.orEmpty(), credential + ) + } + binding.commentContainer.visibility = View.VISIBLE + + //Focus the comments + binding.scrollview.requestChildFocus(binding.commentContainer, binding.commentContainer) + } catch (exception: IOException) { + Log.e("COMMENT FETCH ERROR", exception.toString()) + } catch (exception: HttpException) { + Log.e("COMMENT ERROR", "${exception.code()} with body ${exception.response()?.errorBody()}") + } + } + } + } + + private suspend fun postComment( + api: PixelfedAPI, + credential: String, + ) { + val textIn = binding.editComment.text + val nonNullText = textIn.toString() + status.id.let { + try { + val response = api.postStatus(credential, nonNullText, it) + binding.commentIn.visibility = View.GONE + + //Add the comment to the comment section + addComment( + binding.root.context, binding.commentContainer, response.account!!.username!!, + response.content!!, response.mentions.orEmpty(), credential + ) + + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.comment_posted).format(textIn), + Toast.LENGTH_SHORT + ).show() + } catch (exception: IOException) { + Log.e("COMMENT ERROR", exception.toString()) + Toast.makeText( + binding.root.context, binding.root.context.getString(R.string.comment_error), + Toast.LENGTH_SHORT + ).show() + } catch (exception: HttpException) { + Toast.makeText( + binding.root.context, binding.root.context.getString(R.string.comment_error), + Toast.LENGTH_SHORT + ).show() + Log.e("ERROR_CODE", exception.code().toString()) + } + } + } + } diff --git a/app/src/main/java/com/h/pixeldroid/posts/PostFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/PostFragment.kt deleted file mode 100644 index 52670064..00000000 --- a/app/src/main/java/com/h/pixeldroid/posts/PostFragment.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.h.pixeldroid.posts - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope -import com.h.pixeldroid.databinding.PostFragmentBinding -import com.h.pixeldroid.utils.api.objects.Status -import com.h.pixeldroid.utils.api.objects.Status.Companion.DOMAIN_TAG -import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG -import com.h.pixeldroid.utils.BaseFragment -import com.h.pixeldroid.utils.bindingLifecycleAware - - -class PostFragment : BaseFragment() { - - private lateinit var statusDomain: String - private var currentStatus: Status? = null - - var binding: PostFragmentBinding by bindingLifecycleAware() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - currentStatus = arguments?.getSerializable(POST_TAG) as Status? - statusDomain = arguments?.getString(DOMAIN_TAG)!! - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = PostFragmentBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db) - - val holder = StatusViewHolder(binding) - - holder.bind(currentStatus, api, db, lifecycleScope) - } - -} diff --git a/app/src/main/java/com/h/pixeldroid/posts/StatusViewHolder.kt b/app/src/main/java/com/h/pixeldroid/posts/StatusViewHolder.kt index bca3cd68..f25a8160 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/StatusViewHolder.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/StatusViewHolder.kt @@ -3,9 +3,7 @@ package com.h.pixeldroid.posts import android.Manifest import android.app.AlertDialog import android.content.Intent -import android.graphics.Color import android.graphics.Typeface -import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.text.method.LinkMovementMethod import android.util.Log @@ -19,15 +17,17 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.bumptech.glide.RequestBuilder import com.google.android.material.snackbar.Snackbar -import com.google.android.material.tabs.TabLayoutMediator import com.h.pixeldroid.R import com.h.pixeldroid.databinding.AlbumImageViewBinding -import com.h.pixeldroid.databinding.CommentBinding import com.h.pixeldroid.databinding.PostFragmentBinding +import com.h.pixeldroid.utils.BlurHashDecoder import com.h.pixeldroid.utils.ImageConverter import com.h.pixeldroid.utils.api.PixelfedAPI import com.h.pixeldroid.utils.api.objects.Attachment import com.h.pixeldroid.utils.api.objects.Status +import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_COMMENT_TAG +import com.h.pixeldroid.utils.api.objects.Status.Companion.POST_TAG +import com.h.pixeldroid.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG import com.h.pixeldroid.utils.db.AppDatabase import com.karumi.dexter.Dexter import com.karumi.dexter.listener.PermissionDeniedResponse @@ -36,6 +36,7 @@ import com.karumi.dexter.listener.single.BasePermissionListener import kotlinx.coroutines.launch import retrofit2.HttpException import java.io.IOException +import kotlin.math.roundToInt /** @@ -45,25 +46,36 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold private var status: Status? = null - fun bind(status: Status?, pixelfedAPI: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope) { + fun bind(status: Status?, pixelfedAPI: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair, isActivity: Boolean = false) { this.itemView.visibility = View.VISIBLE this.status = status - val metrics = itemView.context.resources.displayMetrics - //Limit the height of the different images - binding.postPicture.maxHeight = metrics.heightPixels * 3/4 + val maxImageRatio: Float = status?.media_attachments?.map { + if (it.meta?.original?.width == null || it.meta.original.height == null) { + 1f + } else { + it.meta.original.width.toFloat() / it.meta.original.height.toFloat() + } + }?.maxOrNull() ?: 1f + + val (displayWidth, displayHeight) = displayDimensionsInPx + if (displayWidth / maxImageRatio > displayHeight * 3/4f) { + binding.postPager.layoutParams.width = ((displayHeight * 3 / 4f) * maxImageRatio).roundToInt() + binding.postPager.layoutParams.height = (displayHeight * 3 / 4f).toInt() + } else { + binding.postPager.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + binding.postPager.layoutParams.height = (displayWidth / maxImageRatio).toInt() + } //Setup the post layout - val picRequest = Glide.with(itemView) - .asDrawable().fitCenter() - .placeholder(ColorDrawable(Color.GRAY)) + val picRequest = Glide.with(itemView).asDrawable().fitCenter() val user = db.userDao().getActiveUser()!! - setupPost(picRequest, user.instance_uri, false) + setupPost(picRequest, user.instance_uri, isActivity) - activateButtons(pixelfedAPI, db, lifecycleScope) + activateButtons(pixelfedAPI, db, lifecycleScope, isActivity) } @@ -76,7 +88,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold binding.username.apply { text = status?.account?.getDisplayName() ?: "" setTypeface(null, Typeface.BOLD) - setOnClickListener { status?.account?.openProfile(rootView.context) } + setOnClickListener { status?.account?.openProfile(binding.root.context) } } binding.usernameDesc.apply { @@ -85,12 +97,12 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold } binding.nlikes.apply { - text = status?.getNLikes(rootView.context) + text = status?.getNLikes(binding.root.context) setTypeface(null, Typeface.BOLD) } binding.nshares.apply { - text = status?.getNShares(rootView.context) + text = status?.getNShares(binding.root.context) setTypeface(null, Typeface.BOLD) } @@ -116,15 +128,9 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold if(!status?.media_attachments.isNullOrEmpty()) { setupPostPics(binding, request) } else { - binding.postPicture.visibility = View.GONE binding.postPager.visibility = View.GONE - binding.postTabs.visibility = View.GONE + binding.postIndicator.visibility = View.GONE } - - - //Set comment initial visibility - binding.commentIn.visibility = View.GONE - binding.commentContainer.visibility = View.GONE } private fun setupPostPics( @@ -133,50 +139,44 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold ) { // Standard layout - binding.postPicture.visibility = View.VISIBLE - binding.postPager.visibility = View.GONE - binding.postTabs.visibility = View.GONE + binding.postPager.visibility = View.VISIBLE + //Attach the given tabs to the view pager + binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList(), status?.sensitive) - if(status?.media_attachments?.size == 1) { - request.load(status?.getPostUrl()).into(binding.postPicture) - val imgDescription = status?.media_attachments?.get(0)?.description.orEmpty().ifEmpty { binding.root.context.getString( - R.string.no_description) } - binding.postPicture.contentDescription = imgDescription - - binding.postPicture.setOnLongClickListener { - Snackbar.make(it, imgDescription, Snackbar.LENGTH_SHORT).show() - true - } - - } else if(status?.media_attachments?.size!! > 1) { - setupTabsLayout(binding, request) + if(status?.media_attachments?.size ?: 0 > 1) { + binding.postIndicator.setViewPager(binding.postPager) + binding.postIndicator.visibility = View.VISIBLE + } else { + binding.postIndicator.visibility = View.GONE } - if (status?.sensitive!!) { - status?.setupSensitiveLayout(binding) + if (status?.sensitive == true) { + setupSensitiveLayout() + } else { + // GONE is the default, but have to set it again because of how RecyclerViews work + binding.sensitiveWarning.visibility = View.GONE } } - private fun setupTabsLayout( - binding: PostFragmentBinding, - request: RequestBuilder, - ) { - //Only show the viewPager and tabs - binding.postPicture.visibility = View.GONE - binding.postPager.visibility = View.VISIBLE - binding.postTabs.visibility = View.VISIBLE - //Attach the given tabs to the view pager - binding.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList()) + private fun setupSensitiveLayout() { - TabLayoutMediator(binding.postTabs, binding.postPager) { tab, _ -> - tab.icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_dot_blue_12dp) - }.attach() + // Set dark layout and warning message + binding.sensitiveWarning.visibility = View.VISIBLE + //binding.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix) + + fun uncensorPicture(binding: PostFragmentBinding) { + binding.sensitiveWarning.visibility = View.GONE + (binding.postPager.adapter as AlbumViewPagerAdapter).uncensor() + } + + binding.sensitiveWarning.setOnClickListener { + uncensorPicture(binding) + } } private fun setDescription( - rootView: View, api: PixelfedAPI, credential: String, lifecycleScope: LifecycleCoroutineScope @@ -189,7 +189,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold status?.content.orEmpty(), status?.mentions, api, - rootView.context, + binding.root.context, credential, lifecycleScope ) @@ -197,13 +197,18 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold } } } - - private fun activateButtons(api: PixelfedAPI, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){ + //region buttons + private fun activateButtons( + api: PixelfedAPI, + db: AppDatabase, + lifecycleScope: LifecycleCoroutineScope, + isActivity: Boolean + ){ val user = db.userDao().getActiveUser()!! val credential = "Bearer ${user.accessToken}" //Set the special HTML text - setDescription(binding.root, api, credential, lifecycleScope) + setDescription(api, credential, lifecycleScope) //Activate onclickListeners activateLiker( @@ -214,9 +219,23 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold api, credential, status?.reblogged ?: false, lifecycleScope ) - activateCommenter(api, credential, lifecycleScope) - showComments(api, credential, lifecycleScope) + if(isActivity){ + binding.commenter.visibility = View.INVISIBLE + } + else { + binding.commenter.setOnClickListener { + lifecycleScope.launchWhenCreated { + //Open status in activity + val intent = Intent(it.context, PostActivity::class.java) + intent.putExtra(POST_TAG, status) + intent.putExtra(POST_COMMENT_TAG, true) + it.context.startActivity(intent) + } + } + } + + showComments(lifecycleScope, isActivity) activateMoreButton(api, db, lifecycleScope) } @@ -438,7 +457,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold //Activate double tap liking var clicked = false - binding.postPicture.setOnClickListener { + binding.postPager.setOnClickListener { lifecycleScope.launchWhenCreated { //Check that the post isn't hidden if(binding.sensitiveWarning.visibility == View.GONE) { @@ -458,7 +477,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold clicked = true //Reset clicked to false after 500ms - binding.postPicture.handler.postDelayed(fun() { clicked = false }, 500) + binding.postPager.handler.postDelayed(fun() { clicked = false }, 500) } } @@ -511,156 +530,37 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold } } } + //endregion private fun showComments( - api: PixelfedAPI, - credential: String, - lifecycleScope: LifecycleCoroutineScope + lifecycleScope: LifecycleCoroutineScope, + isActivity: Boolean ) { - //Show all comments of a post + //Show number of comments on the post if (status?.replies_count == 0) { binding.viewComments.text = binding.root.context.getString(R.string.NoCommentsToShow) } else { binding.viewComments.apply { - text = binding.root.context.getString(R.string.number_comments) - .format(status?.replies_count) - setOnClickListener { - visibility = View.GONE - - lifecycleScope.launchWhenCreated { - //Retrieve the comments - retrieveComments(api, credential) + text = resources.getQuantityString(R.plurals.number_comments, + status?.replies_count ?: 0, + status?.replies_count ?: 0 + ) + if(!isActivity) { + setOnClickListener { + lifecycleScope.launchWhenCreated { + //Open status in activity + val intent = Intent(context, PostActivity::class.java) + intent.putExtra(POST_TAG, status) + intent.putExtra(VIEW_COMMENTS_TAG, true) + context.startActivity(intent) + } } } } } } - private fun activateCommenter( - api: PixelfedAPI, - credential: String, - lifecycleScope: LifecycleCoroutineScope - ) { - //Toggle comment button - toggleCommentInput() - //Activate commenterpostPicture - binding.submitComment.setOnClickListener { - val textIn = binding.editComment.text - //Open text input - if(textIn.isNullOrEmpty()) { - Toast.makeText( - binding.root.context, - binding.root.context.getString(R.string.empty_comment), - Toast.LENGTH_SHORT - ).show() - } else { - //Post the comment - lifecycleScope.launchWhenCreated { - postComment(api, credential) - } - } - } - } - - private fun toggleCommentInput() { - //Toggle comment button - binding.commenter.setOnClickListener { - when(binding.commentIn.visibility) { - View.VISIBLE -> { - binding.commentIn.visibility = View.GONE - ImageConverter.setImageFromDrawable( - binding.root, - binding.commenter, - R.drawable.ic_comment_empty - ) - } - View.GONE -> { - binding.commentIn.visibility = View.VISIBLE - ImageConverter.setImageFromDrawable( - binding.root, - binding.commenter, - R.drawable.ic_comment_blue - ) - } - } - } - } - - fun addComment(context: android.content.Context, commentContainer: LinearLayout, commentUsername: String, commentContent: String) { - - - val itemBinding = CommentBinding.inflate( - LayoutInflater.from(context), commentContainer, false - ) - - itemBinding.user.text = commentUsername - itemBinding.commentText.text = commentContent - } - - private suspend fun retrieveComments( - api: PixelfedAPI, - credential: String, - ) { - status?.id?.let { - try { - val statuses = api.statusComments(it, credential).descendants - - binding.commentContainer.removeAllViews() - - //Create the new views for each comment - for (status in statuses) { - addComment(binding.root.context, binding.commentContainer, status.account!!.username!!, - status.content!! - ) - } - binding.commentContainer.visibility = View.VISIBLE - - } catch (exception: IOException) { - Log.e("COMMENT FETCH ERROR", exception.toString()) - } catch (exception: HttpException) { - Log.e("COMMENT ERROR", "${exception.code()} with body ${exception.response()?.errorBody()}") - } - } - } - - private suspend fun postComment( - api: PixelfedAPI, - credential: String, - ) { - val textIn = binding.editComment.text - val nonNullText = textIn.toString() - status?.id?.let { - try { - val response = api.postStatus(credential, nonNullText, it) - binding.commentIn.visibility = View.GONE - - //Add the comment to the comment section - addComment( - binding.root.context, binding.commentContainer, response.account!!.username!!, - response.content!! - ) - - Toast.makeText( - binding.root.context, - binding.root.context.getString(R.string.comment_posted).format(textIn), - Toast.LENGTH_SHORT - ).show() - } catch (exception: IOException) { - Log.e("COMMENT ERROR", exception.toString()) - Toast.makeText( - binding.root.context, binding.root.context.getString(R.string.comment_error), - Toast.LENGTH_SHORT - ).show() - } catch (exception: HttpException) { - Toast.makeText( - binding.root.context, binding.root.context.getString(R.string.comment_error), - Toast.LENGTH_SHORT - ).show() - Log.e("ERROR_CODE", exception.code().toString()) - } - } - } companion object { @@ -673,7 +573,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold } } -class AlbumViewPagerAdapter(private val media_attachments: List) : +private class AlbumViewPagerAdapter(private val media_attachments: List, private var sensitive: Boolean?) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -685,19 +585,42 @@ class AlbumViewPagerAdapter(private val media_attachments: List) : override fun getItemCount() = media_attachments.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { - Glide.with(holder.binding.root) - .asDrawable().fitCenter().placeholder(ColorDrawable(Color.GRAY)) - .load(media_attachments[position].url).into(holder.image) + media_attachments[position].apply { + val blurhashBitMap = blurhash?.let { + BlurHashDecoder.blurHashBitmap( + holder.binding.root.resources, + it, + meta?.original?.width, + meta?.original?.height + ) + } + if (sensitive == false) { + Glide.with(holder.binding.root) + .asDrawable().fitCenter() + .placeholder(blurhashBitMap) + .load(url).into(holder.image) + } else { + Glide.with(holder.binding.root) + .asDrawable().fitCenter() + .load(blurhashBitMap).into(holder.image) + } - val description = media_attachments[position].description - .orEmpty().ifEmpty{ holder.binding.root.context.getString(R.string.no_description)} + val description = description + .orEmpty() + .ifEmpty { holder.binding.root.context.getString(R.string.no_description) } - holder.image.setOnLongClickListener { - Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show() - true + holder.image.setOnLongClickListener { + Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show() + true + } + + holder.image.contentDescription = description } + } - holder.image.contentDescription = description + fun uncensor(){ + sensitive = false + notifyDataSetChanged() } class ViewHolder(val binding: AlbumImageViewBinding) : RecyclerView.ViewHolder(binding.root){ diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt index ff40607c..132fff05 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt @@ -19,6 +19,7 @@ import com.h.pixeldroid.posts.feeds.cachedFeeds.CachedFeedFragment import com.h.pixeldroid.posts.feeds.cachedFeeds.ViewModelFactory import com.h.pixeldroid.utils.api.objects.FeedContentDatabase import com.h.pixeldroid.utils.api.objects.Status +import com.h.pixeldroid.utils.displayDimensionsInPx /** @@ -35,7 +36,7 @@ class PostFeedFragment: CachedFeedFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - adapter = PostsAdapter() + adapter = PostsAdapter(requireContext().displayDimensionsInPx()) @Suppress("UNCHECKED_CAST") if (requireArguments().get("home") as Boolean){ @@ -67,7 +68,7 @@ class PostFeedFragment: CachedFeedFragment() { return view } - inner class PostsAdapter : PagingDataAdapter( + inner class PostsAdapter(private val displayDimensionsInPx: Pair) : PagingDataAdapter( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { return oldItem.id == newItem.id @@ -89,7 +90,7 @@ class PostFeedFragment: CachedFeedFragment() { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val uiModel = getItem(position) as Status uiModel.let { - (holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope) + (holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope, displayDimensionsInPx) } } } diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/AccountListFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/AccountListFragment.kt index 6442d8cf..90a5a18b 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/AccountListFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/AccountListFragment.kt @@ -97,7 +97,7 @@ class AccountViewHolder(binding: AccountListEntryBinding) : RecyclerView.ViewHol .circleCrop().placeholder(R.drawable.ic_default_user) .into(avatar) - username.text = account?.username + username.text = account?.display_name @SuppressLint("SetTextI18n") acct.text = "@${account?.acct}" } diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/FollowersPagingSource.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/FollowersPagingSource.kt index 204b3ae3..d1eb8d8b 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/FollowersPagingSource.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/accountLists/FollowersPagingSource.kt @@ -1,18 +1,20 @@ package com.h.pixeldroid.posts.feeds.uncachedFeeds.accountLists import androidx.paging.PagingSource +import androidx.paging.PagingState import com.h.pixeldroid.utils.api.PixelfedAPI import com.h.pixeldroid.utils.api.objects.Account import retrofit2.HttpException import java.io.IOException +import java.math.BigInteger class FollowersPagingSource( private val api: PixelfedAPI, private val accessToken: String, private val accountId: String, private val following: Boolean -) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { +) : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { val position = params.key return try { val response = @@ -23,14 +25,14 @@ class FollowersPagingSource( api.followers(account_id = accountId, authorization = "Bearer $accessToken", limit = params.loadSize, - page = position?.toString(), - max_id = position?.toString()) + page = position, + max_id = position) } else { api.following(account_id = accountId, authorization = "Bearer $accessToken", limit = params.loadSize, - page = position?.toString(), - max_id = position?.toString()) + page = position, + max_id = position) } val accounts = if(response.isSuccessful){ @@ -39,19 +41,19 @@ class FollowersPagingSource( throw HttpException(response) } - val nextPosition = if(response.headers()["Link"] != null){ + val nextPosition: String = if(response.headers()["Link"] != null){ //Header is of the form: // Link: ; rel="next", ; rel="prev" // So we want the first max_id value. In case there are arguments after // the max_id in the URL, we make sure to stop at the first '?' response.headers()["Link"] .orEmpty() - .substringAfter("max_id=") - .substringBefore('?') - .substringBefore('>') - .toIntOrNull() ?: 0 + .substringAfter("max_id=", "") + .substringBefore('?', "") + .substringBefore('>', "") } else { - params.key?.plus(1) ?: 2 + // No Link header, so we just increment the position value + (position?.toBigIntegerOrNull() ?: 1.toBigInteger()).inc().toString() } LoadResult.Page( @@ -65,4 +67,9 @@ class FollowersPagingSource( LoadResult.Error(exception) } } + + override fun getRefreshKey(state: PagingState): String? = + state.anchorPosition?.run { + state.closestItemToPosition(this)?.id + } } \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchPagingSource.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchPagingSource.kt index 1d62aba3..63c4f87f 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchPagingSource.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchPagingSource.kt @@ -1,6 +1,7 @@ package com.h.pixeldroid.posts.feeds.uncachedFeeds.search import androidx.paging.PagingSource +import androidx.paging.PagingState import com.h.pixeldroid.utils.api.PixelfedAPI import com.h.pixeldroid.utils.api.objects.FeedContent import com.h.pixeldroid.utils.api.objects.Results @@ -44,4 +45,9 @@ class SearchPagingSource( LoadResult.Error(exception) } } + + override fun getRefreshKey(state: PagingState): Int? = + state.anchorPosition?.run { + state.closestItemToPosition(this)?.id?.toIntOrNull() + } } \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchPostsFragment.kt b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchPostsFragment.kt index b93cb8ad..bcb7c69b 100644 --- a/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchPostsFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/posts/feeds/uncachedFeeds/search/SearchPostsFragment.kt @@ -15,6 +15,7 @@ import com.h.pixeldroid.posts.StatusViewHolder import com.h.pixeldroid.posts.feeds.uncachedFeeds.* import com.h.pixeldroid.utils.api.objects.Results import com.h.pixeldroid.utils.api.objects.Status +import com.h.pixeldroid.utils.displayDimensionsInPx /** * Fragment to show a list of [Status]es, as a result of a search. @@ -25,7 +26,7 @@ class SearchPostsFragment : UncachedFeedFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - adapter = PostsAdapter() + adapter = PostsAdapter(requireContext().displayDimensionsInPx()) query = arguments?.getSerializable("searchFeed") as String @@ -57,7 +58,7 @@ class SearchPostsFragment : UncachedFeedFragment() { return view } - inner class PostsAdapter : PagingDataAdapter( + inner class PostsAdapter(private val displayDimensionsInPx: Pair) : PagingDataAdapter( object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean { return oldItem.id == newItem.id @@ -79,7 +80,7 @@ class SearchPostsFragment : UncachedFeedFragment() { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val uiModel = getItem(position) as Status uiModel.let { - (holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope) + (holder as StatusViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db, lifecycleScope, displayDimensionsInPx) } } } diff --git a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt index 39bf763a..427c0da0 100644 --- a/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt +++ b/app/src/main/java/com/h/pixeldroid/profile/ProfileActivity.kt @@ -247,14 +247,23 @@ class ProfileActivity : BaseActivity() { supportActionBar?.subtitle = "@${account.acct}" } - binding.nbPostsTextView.text = applicationContext.getString(R.string.nb_posts) - .format(account.statuses_count.toString()) + binding.nbPostsTextView.text = resources.getQuantityString( + R.plurals.nb_posts, + account.statuses_count ?: 0, + account.statuses_count ?: 0 + ) - binding.nbFollowersTextView.text = applicationContext.getString(R.string.nb_followers) - .format(account.followers_count.toString()) + binding.nbFollowersTextView.text = resources.getQuantityString( + R.plurals.nb_followers, + account.followers_count ?: 0, + account.followers_count ?: 0 + ) - binding.nbFollowingTextView.text = applicationContext.getString(R.string.nb_following) - .format(account.following_count.toString()) + binding.nbFollowingTextView.text = resources.getQuantityString( + R.plurals.nb_following, + account.following_count ?: 0, + account.following_count ?: 0 + ) } private fun onClickEditButton() { diff --git a/app/src/main/java/com/h/pixeldroid/searchDiscover/SearchDiscoverFragment.kt b/app/src/main/java/com/h/pixeldroid/searchDiscover/SearchDiscoverFragment.kt index 855fa03d..966e9846 100644 --- a/app/src/main/java/com/h/pixeldroid/searchDiscover/SearchDiscoverFragment.kt +++ b/app/src/main/java/com/h/pixeldroid/searchDiscover/SearchDiscoverFragment.kt @@ -4,25 +4,17 @@ import android.app.SearchManager import android.content.Context import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* import androidx.annotation.StringRes -import androidx.appcompat.widget.SearchView -import androidx.constraintlayout.motion.widget.MotionLayout import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.h.pixeldroid.R import com.h.pixeldroid.databinding.FragmentSearchBinding -import com.h.pixeldroid.databinding.PostFragmentBinding import com.h.pixeldroid.profile.ProfilePostViewHolder import com.h.pixeldroid.utils.api.PixelfedAPI -import com.h.pixeldroid.utils.api.objects.DiscoverPost -import com.h.pixeldroid.utils.api.objects.DiscoverPosts import com.h.pixeldroid.utils.api.objects.Status import com.h.pixeldroid.posts.PostActivity import com.h.pixeldroid.utils.BaseFragment @@ -34,10 +26,7 @@ import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.color import com.mikepenz.iconics.utils.paddingDp import com.mikepenz.iconics.utils.sizeDp -import retrofit2.Call -import retrofit2.Callback import retrofit2.HttpException -import retrofit2.Response import java.io.IOException /** @@ -122,12 +111,12 @@ class SearchDiscoverFragment : BaseFragment() { } /** - * [RecyclerView.Adapter] that can display a list of [DiscoverPost]s + * [RecyclerView.Adapter] that can display a list of [Status]s' thumbnails for the discover view */ class DiscoverRecyclerViewAdapter: RecyclerView.Adapter() { - private val posts: ArrayList = ArrayList() + private val posts: ArrayList = ArrayList() - fun addPosts(newPosts : List) { + fun addPosts(newPosts : List) { posts.clear() posts.addAll(newPosts) notifyDataSetChanged() @@ -141,15 +130,15 @@ class SearchDiscoverFragment : BaseFragment() { override fun onBindViewHolder(holder: ProfilePostViewHolder, position: Int) { val post = posts[position] - if(post.type?.contains("album") == true) { + if(post.media_attachments?.size ?: 0 > 1) { holder.albumIcon.visibility = View.VISIBLE } else { holder.albumIcon.visibility = View.GONE } - ImageConverter.setSquareImageFromURL(holder.postView, post.thumb, holder.postPreview) + ImageConverter.setSquareImageFromURL(holder.postView, post.media_attachments?.firstOrNull()?.preview_url, holder.postPreview, post.media_attachments?.firstOrNull()?.blurhash) holder.postPreview.setOnClickListener { val intent = Intent(holder.postView.context, PostActivity::class.java) - intent.putExtra(Status.DISCOVER_TAG, post) + intent.putExtra(Status.POST_TAG, post) holder.postView.context.startActivity(intent) } } diff --git a/app/src/main/java/com/h/pixeldroid/utils/BlurHashDecoder.kt b/app/src/main/java/com/h/pixeldroid/utils/BlurHashDecoder.kt new file mode 100644 index 00000000..847edc45 --- /dev/null +++ b/app/src/main/java/com/h/pixeldroid/utils/BlurHashDecoder.kt @@ -0,0 +1,145 @@ +package com.h.pixeldroid.utils + +/** + * Blurhash implementation from blurhash project: + * https://github.com/woltapp/blurhash + * Minor modifications by charlag, for the Tusky project + * https://github.com/tuskyapp/Tusky/ + */ + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.BitmapDrawable +import com.h.pixeldroid.utils.api.objects.Attachment +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.withSign + +object BlurHashDecoder { + + fun blurHashBitmap(resources: Resources, blurHash: String, width: Int?, height: Int?): BitmapDrawable { + val ratioOr0 = (width?.toFloat() ?: 1f) / (height?.toFloat() ?: 1f) + + val ratio = if (ratioOr0 == 0f) 1f else ratioOr0 + return BitmapDrawable(resources, + decode(blurHash, + (32f * ratio).toInt().coerceAtLeast(32), + (32f / ratio).toInt().coerceAtLeast(32)) + ) + } + + fun decode(blurHash: String?, width: Int?, height: Int?, punch: Float = 1f): Bitmap? { + if (blurHash == null || width == null || height == null || blurHash.length < 6) { + return null + } + require(width > 0) { "Width must be greater than zero" } + require(height > 0) { "height must be greater than zero" } + + val numCompEnc = decode83(blurHash, 0, 1) + val numCompX = (numCompEnc % 9) + 1 + val numCompY = (numCompEnc / 9) + 1 + if (blurHash.length != 4 + 2 * numCompX * numCompY) { + return null + } + val maxAcEnc = decode83(blurHash, 1, 2) + val maxAc = (maxAcEnc + 1) / 166f + val colors = Array(numCompX * numCompY) { i -> + if (i == 0) { + val colorEnc = decode83(blurHash, 2, 6) + decodeDc(colorEnc) + } else { + val from = 4 + i * 2 + val colorEnc = decode83(blurHash, from, from + 2) + decodeAc(colorEnc, maxAc * punch) + } + } + return composeBitmap(width, height, numCompX, numCompY, colors) + } + + private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { + var result = 0 + for (i in from until to) { + val index = charMap[str[i]] ?: -1 + if (index != -1) { + result = result * 83 + index + } + } + return result + } + + private fun decodeDc(colorEnc: Int): FloatArray { + val r = colorEnc shr 16 + val g = (colorEnc shr 8) and 255 + val b = colorEnc and 255 + return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)) + } + + private fun srgbToLinear(colorEnc: Int): Float { + val v = colorEnc / 255f + return if (v <= 0.04045f) { + (v / 12.92f) + } else { + ((v + 0.055f) / 1.055f).pow(2.4f) + } + } + + private fun decodeAc(value: Int, maxAc: Float): FloatArray { + val r = value / (19 * 19) + val g = (value / 19) % 19 + val b = value % 19 + return floatArrayOf( + signedPow2((r - 9) / 9.0f) * maxAc, + signedPow2((g - 9) / 9.0f) * maxAc, + signedPow2((b - 9) / 9.0f) * maxAc + ) + } + + private fun signedPow2(value: Float) = value.pow(2f).withSign(value) + + private fun composeBitmap( + width: Int, height: Int, + numCompX: Int, numCompY: Int, + colors: Array + ): Bitmap { + val imageArray = IntArray(width * height) + for (y in 0 until height) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + for (i in 0 until numCompX) { + val basis = (cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat() + val color = colors[j * numCompX + i] + r += color[0] * basis + g += color[1] * basis + b += color[2] * basis + } + } + imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + } + } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) + } + + private fun linearToSrgb(value: Float): Int { + val v = value.coerceIn(0f, 1f) + return if (v <= 0.0031308f) { + (v * 12.92f * 255f + 0.5f).toInt() + } else { + ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() + } + } + + private val charMap = listOf( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', + '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' + ) + .mapIndexed { i, c -> c to i } + .toMap() +} \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/utils/ImageConverter.kt b/app/src/main/java/com/h/pixeldroid/utils/ImageConverter.kt index 9e80fde7..305e4196 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/ImageConverter.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/ImageConverter.kt @@ -69,8 +69,10 @@ class ImageConverter { * @param url, the url of the image that will be loaded * @param image, the imageView into which we will load the image */ - fun setSquareImageFromURL(view : View, url : String?, image : ImageView) { - Glide.with(view).load(url).apply(RequestOptions().centerCrop()).into(image) + fun setSquareImageFromURL(view : View, url : String?, image : ImageView, blurhash: String? = null) { + Glide.with(view).load(url).placeholder( + blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32,32) } + ).apply(RequestOptions().centerCrop()).into(image) } diff --git a/app/src/main/java/com/h/pixeldroid/utils/Utils.kt b/app/src/main/java/com/h/pixeldroid/utils/Utils.kt index 03a3b359..2431574b 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/Utils.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/Utils.kt @@ -8,12 +8,15 @@ import android.content.res.Resources import android.net.ConnectivityManager import android.net.Uri import android.os.Build +import android.util.DisplayMetrics +import android.view.WindowManager import androidx.appcompat.app.AppCompatDelegate import androidx.browser.customtabs.CustomTabsIntent import androidx.fragment.app.Fragment import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.h.pixeldroid.R +import okhttp3.HttpUrl import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @@ -22,6 +25,35 @@ fun hasInternet(context: Context): Boolean { return cm.activeNetwork != null } +/** + * Check if domain is valid or not + */ +fun validDomain(domain: String?): Boolean { + domain?.apply { + try { + HttpUrl.Builder().host(replace("https://", "")).scheme("https").build() + } catch (e: IllegalArgumentException) { + return false + } + } ?: return false + + return true +} + + +fun Context.displayDimensionsInPx(): Pair { + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Pair(windowManager.currentWindowMetrics.bounds.width(), windowManager.currentWindowMetrics.bounds.height()) + } else { + val metrics = DisplayMetrics() + @Suppress("DEPRECATION") + windowManager.defaultDisplay.getMetrics(metrics) + Pair(metrics.widthPixels, metrics.heightPixels) + } +} + fun normalizeDomain(domain: String): String { return "https://" + domain .replace("http://", "") diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt b/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt index cbbda235..f4d4d188 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/PixelfedAPI.kt @@ -256,6 +256,7 @@ interface PixelfedAPI { fun mediaUpload( //The authorization header needs to be of the form "Bearer " @Header("Authorization") authorization: String, + @Part description: MultipartBody.Part? = null, @Part file: MultipartBody.Part ): Observable diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/objects/Attachment.kt b/app/src/main/java/com/h/pixeldroid/utils/api/objects/Attachment.kt index 4d9cc2a2..1202f338 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/objects/Attachment.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/objects/Attachment.kt @@ -11,11 +11,31 @@ data class Attachment( //Optional attributes val remote_url: String? = null, //URL val text_url: String? = null, //URL - //TODO meta + + val meta: Meta?, + val description: String? = null, val blurhash: String? = null ) : Serializable { enum class AttachmentType { unknown, image, gifv, video, audio } + + data class Meta ( + val focus: Focus?, + val original: Image? + ) : Serializable + + { + data class Focus( + val x: Double?, + val y: Double? + ) : Serializable + data class Image( + val width: Int?, + val height: Int?, + val size: String?, + val aspect: Double? + ) : Serializable + } } diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/objects/DiscoverPost.kt b/app/src/main/java/com/h/pixeldroid/utils/api/objects/DiscoverPost.kt deleted file mode 100644 index ff1f3b86..00000000 --- a/app/src/main/java/com/h/pixeldroid/utils/api/objects/DiscoverPost.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.h.pixeldroid.utils.api.objects - -import java.io.Serializable - -/* -NOT DOCUMENTED, USE WITH CAUTION - */ - -data class DiscoverPost( - val type: String?, //This is probably an enum, with these values: https://github.com/pixelfed/pixelfed/blob/700c7805cecc364b68b9cfe20df00608e0f6c465/app/Status.php#L31 - val url: String?, //URL to post - val thumb: String? //URL to thumbnail -) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/objects/DiscoverPosts.kt b/app/src/main/java/com/h/pixeldroid/utils/api/objects/DiscoverPosts.kt index 1fdfb94f..cf3c27f6 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/objects/DiscoverPosts.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/objects/DiscoverPosts.kt @@ -4,5 +4,5 @@ import java.io.Serializable data class DiscoverPosts( //Required attributes - val posts: List + val posts: List ) : Serializable diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/objects/Instance.kt b/app/src/main/java/com/h/pixeldroid/utils/api/objects/Instance.kt index b8031b35..bfac27d0 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/objects/Instance.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/objects/Instance.kt @@ -1,5 +1,7 @@ package com.h.pixeldroid.utils.api.objects +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_TOOT_CHARS + data class Instance ( val description: String?, val email: String?, @@ -9,8 +11,4 @@ data class Instance ( val title: String?, val uri: String?, val version: String? -) { - companion object { - const val DEFAULT_MAX_TOOT_CHARS = 500 - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/objects/NodeInfo.kt b/app/src/main/java/com/h/pixeldroid/utils/api/objects/NodeInfo.kt index 32ff27e2..e302c88f 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/objects/NodeInfo.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/objects/NodeInfo.kt @@ -1,5 +1,7 @@ package com.h.pixeldroid.utils.api.objects +import com.h.pixeldroid.utils.validDomain + /* See https://nodeinfo.diaspora.software/schema.html and https://pixelfed.social/api/nodeinfo/2.0.json A lot of attributes we don't need are omitted, if in the future they are needed we @@ -11,8 +13,21 @@ data class NodeInfo ( val software: Software?, val protocols: List?, val openRegistrations: Boolean?, - val metadata: PixelfedMetadata? + val metadata: PixelfedMetadata?, ){ + /** + * Check if this NodeInfo has the fields we need or if we also need to look into the + * /api/v1/instance endpoint + * This only checks for values that might be in the /api/v1/instance endpoint. + */ + fun hasInstanceEndpointInfo(): Boolean { + return validDomain(metadata?.config?.site?.url) + && !metadata?.config?.site?.name.isNullOrBlank() + && metadata?.config?.uploader?.max_caption_length?.toIntOrNull() != null + } + + + data class Software( val name: String?, val version: String? @@ -31,7 +46,8 @@ data class NodeInfo ( val open_registration: Boolean?, val uploader: Uploader?, val activitypub: ActivityPub?, - val features: Features? + val features: Features?, + val site: Site? ){ data class Uploader( val max_photo_size: String?, @@ -55,6 +71,13 @@ data class NodeInfo ( val stories: Boolean?, val video: Boolean? ) + + data class Site( + val name: String?, + val domain: String?, + val url: String?, + val description: String? + ) } } diff --git a/app/src/main/java/com/h/pixeldroid/utils/api/objects/Status.kt b/app/src/main/java/com/h/pixeldroid/utils/api/objects/Status.kt index 23b25f95..9b88e619 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/api/objects/Status.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/api/objects/Status.kt @@ -64,8 +64,8 @@ open class Status( { companion object { const val POST_TAG = "postTag" - const val DOMAIN_TAG = "domainTag" - const val DISCOVER_TAG = "discoverTag" + const val VIEW_COMMENTS_TAG = "view_comments_tag" + const val POST_COMMENT_TAG = "post_comment_tag" } fun getPostUrl() : String? = media_attachments?.firstOrNull()?.url @@ -74,11 +74,19 @@ open class Status( fun getNLikes(context: Context) : CharSequence { - return context.getString(R.string.likes).format(favourites_count.toString()) + return context.resources.getQuantityString( + R.plurals.likes, + favourites_count ?: 0, + favourites_count ?: 0 + ) } fun getNShares(context: Context) : CharSequence { - return context.getString(R.string.shares).format(reblogs_count.toString()) + return context.resources.getQuantityString( + R.plurals.shares, + reblogs_count ?: 0, + reblogs_count ?: 0 + ) } fun getStatusDomain(domain: String) : String { @@ -88,29 +96,6 @@ open class Status( } - fun setupSensitiveLayout(binding: PostFragmentBinding) { - - // Set dark layout and warning message - binding.sensitiveWarning.visibility = VISIBLE - val array = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f, 0f) - val censorMatrix = ColorMatrix(array) - binding.postPicture.colorFilter = ColorMatrixColorFilter(censorMatrix) - - fun uncensorPicture(binding: PostFragmentBinding) { - binding.sensitiveWarning.visibility = GONE - binding.postPicture.clearColorFilter() - } - - - binding.sensitiveWarning.setOnClickListener { - uncensorPicture(binding) - } - - binding.postPicture.setOnClickListener { - uncensorPicture(binding) - } - } - fun downloadImage(context: Context, url: String, view: View, share: Boolean = false) { val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager diff --git a/app/src/main/java/com/h/pixeldroid/utils/db/AppDatabase.kt b/app/src/main/java/com/h/pixeldroid/utils/db/AppDatabase.kt index 876f6bc2..e81e1d1d 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/db/AppDatabase.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/db/AppDatabase.kt @@ -20,7 +20,7 @@ import com.h.pixeldroid.utils.api.objects.Notification PublicFeedStatusDatabaseEntity::class, Notification::class ], - version = 2 + version = 3 ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/com/h/pixeldroid/utils/db/DBUtils.kt b/app/src/main/java/com/h/pixeldroid/utils/db/DBUtils.kt index 506dc799..9193bf07 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/db/DBUtils.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/db/DBUtils.kt @@ -4,23 +4,20 @@ import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity import com.h.pixeldroid.utils.db.entities.UserDatabaseEntity import com.h.pixeldroid.utils.api.objects.Account import com.h.pixeldroid.utils.api.objects.Instance +import com.h.pixeldroid.utils.api.objects.NodeInfo +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_ALBUM_LIMIT +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_PHOTO_SIZE +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_TOOT_CHARS +import com.h.pixeldroid.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_VIDEO_SIZE import com.h.pixeldroid.utils.normalizeDomain - -private fun normalizeOrNot(uri: String): String{ - return if(uri.startsWith("http://localhost")){ - uri - } else { - normalizeDomain(uri) - } -} +import java.lang.IllegalArgumentException fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true, accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) { db.userDao().insertUser( UserDatabaseEntity( user_id = account.id!!, - //make sure not to normalize to https when localhost, to allow testing - instance_uri = normalizeOrNot(instance_uri), + instance_uri = normalizeDomain(instance_uri), username = account.username!!, display_name = account.getDisplayName(), avatar_static = account.avatar_static.orEmpty(), @@ -33,14 +30,24 @@ fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: ) } -fun storeInstance(db: AppDatabase, instance: Instance) { - val maxTootChars = instance.max_toot_chars?.toInt() ?: Instance.DEFAULT_MAX_TOOT_CHARS - val dbInstance = InstanceDatabaseEntity( - //make sure not to normalize to https when localhost, to allow testing - uri = normalizeOrNot(instance.uri.orEmpty()), - title = instance.title.orEmpty(), - max_toot_chars = maxTootChars, - thumbnail = instance.thumbnail.orEmpty() - ) +fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) { + val dbInstance: InstanceDatabaseEntity = nodeInfo?.run { + InstanceDatabaseEntity( + uri = normalizeDomain(metadata?.config?.site?.url!!), + title = metadata.config.site.name!!, + maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(), + maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE, + //Pixelfed doesn't distinguish between max photo and video size + maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE, + albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT + ) + } ?: instance?.run { + InstanceDatabaseEntity( + uri = normalizeDomain(uri.orEmpty()), + title = title.orEmpty(), + maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS, + ) + } ?: throw IllegalArgumentException("Cannot store instance where both are null") + db.instanceDao().insertInstance(dbInstance) } \ No newline at end of file diff --git a/app/src/main/java/com/h/pixeldroid/utils/db/entities/InstanceDatabaseEntity.kt b/app/src/main/java/com/h/pixeldroid/utils/db/entities/InstanceDatabaseEntity.kt index 4b647b2a..2906dcb1 100644 --- a/app/src/main/java/com/h/pixeldroid/utils/db/entities/InstanceDatabaseEntity.kt +++ b/app/src/main/java/com/h/pixeldroid/utils/db/entities/InstanceDatabaseEntity.kt @@ -6,8 +6,23 @@ import com.h.pixeldroid.utils.api.objects.Instance @Entity(tableName = "instances") data class InstanceDatabaseEntity ( - @PrimaryKey var uri: String, - var title: String = "", - var max_toot_chars: Int = Instance.DEFAULT_MAX_TOOT_CHARS, - var thumbnail: String = "" -) \ No newline at end of file + @PrimaryKey var uri: String, + var title: String, + var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS, + // Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB + var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE, + // Mastodon has different file limits for videos, default of 40MB + var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE, + // How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4 + var albumLimit: Int = DEFAULT_ALBUM_LIMIT, +) { + companion object{ + // Default max number of chars for Mastodon: used when their is no other value supplied by + // either NodeInfo or the instance endpoint + const val DEFAULT_MAX_TOOT_CHARS = 500 + + const val DEFAULT_MAX_PHOTO_SIZE = 8000 + const val DEFAULT_MAX_VIDEO_SIZE = 40000 + const val DEFAULT_ALBUM_LIMIT = 4 + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/index.png b/app/src/main/res/drawable-hdpi/index.png deleted file mode 100644 index ab3b833c..00000000 Binary files a/app/src/main/res/drawable-hdpi/index.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/index.png b/app/src/main/res/drawable-mdpi/index.png deleted file mode 100644 index 284db3fb..00000000 Binary files a/app/src/main/res/drawable-mdpi/index.png and /dev/null differ diff --git a/app/src/main/res/drawable-night/theme.xml b/app/src/main/res/drawable-night/theme.xml index 0e4657f0..3a5a6a9a 100644 --- a/app/src/main/res/drawable-night/theme.xml +++ b/app/src/main/res/drawable-night/theme.xml @@ -1,11 +1,8 @@ - - - - - - + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/theme_night.xml b/app/src/main/res/drawable-night/theme_night.xml deleted file mode 100644 index 49bd4b69..00000000 --- a/app/src/main/res/drawable-night/theme_night.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/index.png b/app/src/main/res/drawable-xhdpi/index.png deleted file mode 100644 index 99682f33..00000000 Binary files a/app/src/main/res/drawable-xhdpi/index.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/index.png b/app/src/main/res/drawable-xxhdpi/index.png deleted file mode 100644 index d25db69a..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/index.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/index.png b/app/src/main/res/drawable-xxxhdpi/index.png deleted file mode 100644 index 64ed171f..00000000 Binary files a/app/src/main/res/drawable-xxxhdpi/index.png and /dev/null differ diff --git a/app/src/main/res/drawable/add_photo_alternate_gray_30dp.xml b/app/src/main/res/drawable/add_photo_alternate_gray_30dp.xml new file mode 100644 index 00000000..796fe501 --- /dev/null +++ b/app/src/main/res/drawable/add_photo_alternate_gray_30dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/add_photo_button.xml b/app/src/main/res/drawable/add_photo_button.xml new file mode 100644 index 00000000..be312a2f --- /dev/null +++ b/app/src/main/res/drawable/add_photo_button.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/check_circle_24.xml b/app/src/main/res/drawable/check_circle_24.xml new file mode 100644 index 00000000..3cb7dd9a --- /dev/null +++ b/app/src/main/res/drawable/check_circle_24.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/rounded_corner.xml b/app/src/main/res/drawable/rounded_corner.xml new file mode 100644 index 00000000..fb42acf0 --- /dev/null +++ b/app/src/main/res/drawable/rounded_corner.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_commenter.xml b/app/src/main/res/drawable/selector_commenter.xml new file mode 100644 index 00000000..84248bac --- /dev/null +++ b/app/src/main/res/drawable/selector_commenter.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/theme.xml b/app/src/main/res/drawable/theme.xml index 0e4657f0..58940ff7 100644 --- a/app/src/main/res/drawable/theme.xml +++ b/app/src/main/res/drawable/theme.xml @@ -1,11 +1,8 @@ - + - - - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/theme_night.xml b/app/src/main/res/drawable/theme_night.xml deleted file mode 100644 index 49bd4b69..00000000 --- a/app/src/main/res/drawable/theme_night.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_post.xml b/app/src/main/res/layout/activity_post.xml index befd542c..9108e345 100644 --- a/app/src/main/res/layout/activity_post.xml +++ b/app/src/main/res/layout/activity_post.xml @@ -1,37 +1,59 @@ - + tools:context=".posts.PostActivity" + android:id="@+id/scrollview"> - - - - - - + android:layout_height="wrap_content"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_post_creation.xml b/app/src/main/res/layout/activity_post_creation.xml index a99418c2..cdbb9ecc 100644 --- a/app/src/main/res/layout/activity_post_creation.xml +++ b/app/src/main/res/layout/activity_post_creation.xml @@ -8,14 +8,15 @@ + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible"> + +