mirror of
https://gitlab.shinice.net/pixeldroid/PixelDroid
synced 2025-02-03 17:07:31 +01:00
Merge branch 'database_backed_feeds' into 'master'
Database backed feeds Closes #235 See merge request pixeldroid/PixelDroid!262
This commit is contained in:
commit
b5f42e2662
@ -11,13 +11,14 @@ apply plugin: 'jacoco'
|
||||
android {
|
||||
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion '30.0.1'
|
||||
buildToolsVersion '30.0.2'
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId "com.h.pixeldroid"
|
||||
@ -53,6 +54,9 @@ android {
|
||||
animationsDisabled true
|
||||
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin-kapt'
|
||||
}
|
||||
@ -75,7 +79,7 @@ dependencies {
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.0.0-alpha09'
|
||||
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"
|
||||
@ -83,16 +87,16 @@ dependencies {
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
|
||||
// Use the most recent version of CameraX
|
||||
def camerax_version = '1.0.0-beta11'
|
||||
def camerax_version = '1.0.0-beta12'
|
||||
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-alpha18'
|
||||
implementation 'androidx.camera:camera-view:1.0.0-alpha19'
|
||||
|
||||
def room_version = "2.2.5"
|
||||
def room_version = "2.3.0-alpha03"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
@ -148,7 +152,7 @@ dependencies {
|
||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar'
|
||||
|
||||
|
||||
implementation 'com.karumi:dexter:6.2.1'
|
||||
implementation 'com.karumi:dexter:6.2.2'
|
||||
|
||||
implementation 'com.github.ligi.tracedroid:lib:3.0'
|
||||
implementation 'com.github.ligi.tracedroid:supportemail:3.0'
|
||||
|
@ -630,8 +630,13 @@
|
||||
license: The Apache Software License, Version 2.0
|
||||
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
url: https://github.com/Kotlin/kotlinx.coroutines
|
||||
- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core:+
|
||||
name: kotlinx-coroutines-core
|
||||
- artifact: androidx.databinding:viewbinding:+
|
||||
name: viewbinding
|
||||
copyrightHolder: Google Inc.
|
||||
license: The Apache Software License, Version 2.0
|
||||
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:+
|
||||
name: kotlinx-coroutines-core-jvm
|
||||
copyrightHolder: JetBrains s.r.o. and contributors
|
||||
license: The Apache Software License, Version 2.0
|
||||
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
|
@ -9,8 +9,8 @@ import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.fragments.CameraFragment
|
||||
import com.h.pixeldroid.testUtility.clearData
|
||||
import com.h.pixeldroid.testUtility.initDB
|
||||
|
@ -13,8 +13,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.testUtility.MockServer
|
||||
import com.h.pixeldroid.testUtility.clearData
|
||||
import com.h.pixeldroid.testUtility.initDB
|
||||
|
@ -2,12 +2,9 @@ package com.h.pixeldroid
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.TextView
|
||||
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.action.ViewActions.swipeUp
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
|
||||
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
|
||||
@ -15,9 +12,9 @@ import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.PostViewHolder
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.fragments.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
|
||||
@ -99,10 +96,10 @@ class HomeFeedTest {
|
||||
@Test
|
||||
fun clickingLikeButtonWorks() {
|
||||
onView(withId(R.id.list)).perform(
|
||||
actionOnItemAtPosition<PostViewHolder>(0, clickChildViewWithId(R.id.liker))
|
||||
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.liker))
|
||||
)
|
||||
onView(withId(R.id.list)).perform(
|
||||
actionOnItemAtPosition<PostViewHolder>(0, clickChildViewWithId(R.id.liker))
|
||||
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.liker))
|
||||
)
|
||||
onView(first(withId(R.id.nlikes)))
|
||||
.check(matches(withText(getText(first(withId(R.id.nlikes))))))
|
||||
@ -111,7 +108,7 @@ class HomeFeedTest {
|
||||
@Test
|
||||
fun clickingLikeButtonFails() {
|
||||
onView(withId(R.id.list)).perform(
|
||||
actionOnItemAtPosition<PostViewHolder>(2, clickChildViewWithId(R.id.liker))
|
||||
actionOnItemAtPosition<StatusViewHolder>(2, clickChildViewWithId(R.id.liker))
|
||||
)
|
||||
onView((withId(R.id.list))).check(matches(isDisplayed()))
|
||||
}
|
||||
@ -119,7 +116,7 @@ class HomeFeedTest {
|
||||
@Test
|
||||
fun clickingUsernameOpensProfile() {
|
||||
onView(withId(R.id.list)).perform(
|
||||
actionOnItemAtPosition<PostViewHolder>(0, clickChildViewWithId(R.id.username))
|
||||
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.username))
|
||||
)
|
||||
onView(withId(R.id.accountNameTextView)).check(matches(isDisplayed()))
|
||||
}
|
||||
@ -127,7 +124,7 @@ class HomeFeedTest {
|
||||
@Test
|
||||
fun clickingProfilePicOpensProfile() {
|
||||
onView(withId(R.id.list)).perform(
|
||||
actionOnItemAtPosition<PostViewHolder>(0, clickChildViewWithId(R.id.profilePic))
|
||||
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.profilePic))
|
||||
)
|
||||
onView(withId(R.id.accountNameTextView)).check(matches(isDisplayed()))
|
||||
}
|
||||
@ -135,10 +132,10 @@ class HomeFeedTest {
|
||||
@Test
|
||||
fun clickingReblogButtonWorks() {
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickChildViewWithId(R.id.reblogger)))
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickChildViewWithId(R.id.reblogger)))
|
||||
onView(first(withId(R.id.nshares)))
|
||||
.check(matches(withText(getText(first(withId(R.id.nshares))))))
|
||||
@ -147,7 +144,7 @@ class HomeFeedTest {
|
||||
@Test
|
||||
fun clickingMentionOpensProfile() {
|
||||
onView(withId(R.id.list)).perform(
|
||||
actionOnItemAtPosition<PostViewHolder>(0, clickChildViewWithId(R.id.description))
|
||||
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.description))
|
||||
)
|
||||
onView(first(withId(R.id.username))).check(matches(isDisplayed()))
|
||||
}
|
||||
@ -155,7 +152,7 @@ class HomeFeedTest {
|
||||
@Test
|
||||
fun clickingHashTagsWorks() {
|
||||
onView(withId(R.id.list)).perform(
|
||||
actionOnItemAtPosition<PostViewHolder>(1, clickChildViewWithId(R.id.description))
|
||||
actionOnItemAtPosition<StatusViewHolder>(1, clickChildViewWithId(R.id.description))
|
||||
)
|
||||
onView(withId(R.id.list)).check(matches(isDisplayed()))
|
||||
}
|
||||
@ -164,7 +161,7 @@ class HomeFeedTest {
|
||||
@Test
|
||||
fun clickingCommentButtonOpensCommentSection() {
|
||||
onView(withId(R.id.list)).perform(
|
||||
actionOnItemAtPosition<PostViewHolder>(0, clickChildViewWithId(R.id.commenter))
|
||||
actionOnItemAtPosition<StatusViewHolder>(0, clickChildViewWithId(R.id.commenter))
|
||||
)
|
||||
onView(first(withId(R.id.commentIn)))
|
||||
.check(matches(hasDescendant(withId(R.id.editComment))))
|
||||
@ -174,7 +171,7 @@ class HomeFeedTest {
|
||||
fun clickingViewCommentShowsTheComments() {
|
||||
//Open the comment section
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickChildViewWithId(R.id.ViewComments)))
|
||||
Thread.sleep(1000)
|
||||
onView(first(withId(R.id.commentContainer)))
|
||||
@ -185,7 +182,7 @@ class HomeFeedTest {
|
||||
fun clickingViewCommentFails() {
|
||||
//Open the comment section
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(2, clickChildViewWithId(R.id.ViewComments)))
|
||||
Thread.sleep(1000)
|
||||
onView(withId(R.id.list)).check(matches(isDisplayed()))
|
||||
@ -195,17 +192,17 @@ class HomeFeedTest {
|
||||
fun postingACommentWorks() {
|
||||
//Open the comment section
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickChildViewWithId(R.id.commenter)))
|
||||
|
||||
onView(withId(R.id.list)).perform(slowSwipeUp(false))
|
||||
Thread.sleep(1000)
|
||||
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, typeTextInViewWithId(R.id.editComment, "test")))
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickChildViewWithId(R.id.submitComment)))
|
||||
|
||||
Thread.sleep(1000)
|
||||
@ -215,14 +212,14 @@ class HomeFeedTest {
|
||||
|
||||
@Test
|
||||
fun performClickOnSensitiveWarning() {
|
||||
onView(withId(R.id.list)).perform(scrollToPosition<PostViewHolder>(1))
|
||||
onView(withId(R.id.list)).perform(scrollToPosition<StatusViewHolder>(1))
|
||||
Thread.sleep(1000)
|
||||
|
||||
onView(second(withId(R.id.sensitiveWarning))).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
|
||||
Thread.sleep(1000)
|
||||
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(1, clickChildViewWithId(R.id.sensitiveWarning)))
|
||||
Thread.sleep(1000)
|
||||
|
||||
@ -232,14 +229,14 @@ class HomeFeedTest {
|
||||
|
||||
@Test
|
||||
fun performClickOnSensitiveWarningTabs() {
|
||||
onView(withId(R.id.list)).perform(scrollToPosition<PostViewHolder>(0))
|
||||
onView(withId(R.id.list)).perform(scrollToPosition<StatusViewHolder>(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<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickChildViewWithId(R.id.sensitiveWarning)))
|
||||
Thread.sleep(1000)
|
||||
|
||||
@ -254,16 +251,16 @@ class HomeFeedTest {
|
||||
|
||||
//Remove sensitive media warning
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickChildViewWithId(R.id.sensitiveWarning)))
|
||||
Thread.sleep(100)
|
||||
|
||||
//Like the post
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickChildViewWithId(R.id.postPicture)))
|
||||
onView(withId(R.id.list))
|
||||
.perform(actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(actionOnItemAtPosition<StatusViewHolder >
|
||||
(0, clickChildViewWithId(R.id.postPicture)))
|
||||
//...
|
||||
Thread.sleep(100)
|
||||
|
@ -24,9 +24,9 @@ import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.PostViewHolder
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.fragments.StatusViewHolder
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_TAG
|
||||
import com.h.pixeldroid.testUtility.MockServer
|
||||
@ -112,7 +112,7 @@ class IntentTest {
|
||||
|
||||
//Click the mention
|
||||
Espresso.onView(ViewMatchers.withId(R.id.list))
|
||||
.perform(RecyclerViewActions.actionOnItemAtPosition<PostViewHolder>
|
||||
.perform(RecyclerViewActions.actionOnItemAtPosition<StatusViewHolder>
|
||||
(0, clickClickableSpanInDescription("@Dobios")))
|
||||
|
||||
//Wait a bit
|
||||
|
@ -17,8 +17,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.testUtility.MockServer
|
||||
import com.h.pixeldroid.testUtility.clearData
|
||||
import com.h.pixeldroid.testUtility.initDB
|
||||
|
@ -1,38 +1,6 @@
|
||||
package com.h.pixeldroid
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorMatrix
|
||||
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.contrib.RecyclerViewActions.actionOnItemAtPosition
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.PostViewHolder
|
||||
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.clickChildViewWithId
|
||||
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.first
|
||||
import com.h.pixeldroid.testUtility.CustomMatchers.Companion.second
|
||||
import com.h.pixeldroid.testUtility.MockServer
|
||||
import com.h.pixeldroid.testUtility.clearData
|
||||
import com.h.pixeldroid.testUtility.initDB
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.censorColorMatrix
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.uncensorColorMatrix
|
||||
import junit.framework.Assert.assertEquals
|
||||
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)
|
||||
|
@ -1,40 +1,5 @@
|
||||
package com.h.pixeldroid
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.core.net.toUri
|
||||
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.adapters.ThumbnailAdapter
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.testUtility.CustomMatchers
|
||||
import com.h.pixeldroid.testUtility.MockServer
|
||||
import com.h.pixeldroid.testUtility.clearData
|
||||
import com.h.pixeldroid.testUtility.initDB
|
||||
import kotlinx.android.synthetic.main.activity_post_creation.*
|
||||
import org.hamcrest.Matchers.not
|
||||
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
|
||||
import java.io.File
|
||||
/*
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PostCreationActivityTest {
|
||||
|
@ -17,8 +17,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.testUtility.MockServer
|
||||
import com.h.pixeldroid.testUtility.clearData
|
||||
import com.h.pixeldroid.testUtility.initDB
|
||||
|
@ -14,8 +14,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.objects.*
|
||||
import com.h.pixeldroid.testUtility.MockServer
|
||||
import com.h.pixeldroid.testUtility.clearData
|
||||
|
@ -25391,7 +25391,254 @@
|
||||
</div>
|
||||
<div class="library">
|
||||
<!-- https://opensource.org/licenses/Apache-2.0 -->
|
||||
<h1 class="title">kotlinx-coroutines-core</h1>
|
||||
<h1 class="title">viewbinding</h1>
|
||||
<p class="notice">Copyright © Google Inc. All rights reserved.</p>
|
||||
|
||||
<input type="checkbox"><label></label>
|
||||
<div class="license">
|
||||
<h2>
|
||||
Apache License
|
||||
<br/>
|
||||
Version 2.0, January 2004
|
||||
<br/>
|
||||
http://www.apache.org/licenses/
|
||||
</h2>
|
||||
<p>TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION</p>
|
||||
<h2>1. Definitions.</h2>
|
||||
<p>
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
</p>
|
||||
<p>
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
</p>
|
||||
<p>
|
||||
"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.
|
||||
</p>
|
||||
<p>
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
</p>
|
||||
<p>
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
</p>
|
||||
<p>
|
||||
"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.
|
||||
</p>
|
||||
<p>
|
||||
"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).
|
||||
</p>
|
||||
<p>
|
||||
"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.
|
||||
</p>
|
||||
<p>
|
||||
"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."
|
||||
</p>
|
||||
<p>
|
||||
"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.
|
||||
</p>
|
||||
<div class="block">
|
||||
<h2 class="inline">2. Grant of Copyright License.</h2>
|
||||
<p class="inline">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2 class="inline">3. Grant of Patent License.</h2>
|
||||
<p class="inline">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2 class="inline">4. Redistribution.</h2>
|
||||
<p class="inline">
|
||||
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:
|
||||
</p>
|
||||
</div>
|
||||
<ul class="low-alpha">
|
||||
<li>
|
||||
You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
</li>
|
||||
<li>
|
||||
You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
</li>
|
||||
<li>
|
||||
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
|
||||
</li>
|
||||
<li>
|
||||
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.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<div class="block">
|
||||
<h2 class="inline">5. Submission of Contributions.</h2>
|
||||
<p class="inline">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2 class="inline">6. Trademarks.</h2>
|
||||
<p class="inline">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2 class="inline">7. Disclaimer of Warranty.</h2>
|
||||
<p class="inline">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2 class="inline">8. Limitation of Liability.</h2>
|
||||
<p class="inline">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="block">
|
||||
<h2 class="inline">9. Accepting Warranty or Additional Liability.</h2>
|
||||
<p class="inline">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<p>END OF TERMS AND CONDITIONS</p>
|
||||
<h1>APPENDIX: How to apply the Apache License to your work.</h1>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<pre>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.</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="library">
|
||||
<!-- https://opensource.org/licenses/Apache-2.0 -->
|
||||
<h1 class="title">kotlinx-coroutines-core-jvm</h1>
|
||||
<p class="notice">Copyright © JetBrains s.r.o. and contributors. All rights reserved.</p>
|
||||
<p><a href="https://github.com/Kotlin/kotlinx.coroutines">https://github.com/Kotlin/kotlinx.coroutines</a></p>
|
||||
<input type="checkbox"><label></label>
|
||||
|
@ -1,20 +1,16 @@
|
||||
package com.h.pixeldroid
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.fragments.feeds.AccountListFragment
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.accountLists.AccountListFragment
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_ID_TAG
|
||||
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_TAG
|
||||
import com.h.pixeldroid.objects.Account.Companion.FOLLOWERS_TAG
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class FollowsActivity : AppCompatActivity() {
|
||||
private var followsFragment = AccountListFragment()
|
||||
@Inject
|
||||
|
@ -8,21 +8,24 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.HomeStatusDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.fragments.CameraFragment
|
||||
import com.h.pixeldroid.fragments.SearchDiscoverFragment
|
||||
import com.h.pixeldroid.fragments.feeds.NotificationsFragment
|
||||
import com.h.pixeldroid.fragments.feeds.OfflineFeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.HomeTimelineFragment
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.PublicTimelineFragment
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.postFeeds.PostFeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.notifications.NotificationsFragment
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.utils.DBUtils
|
||||
import com.h.pixeldroid.utils.Utils.Companion.hasInternet
|
||||
@ -40,11 +43,11 @@ import org.ligi.tracedroid.sending.TraceDroidEmailSender
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.lang.IllegalArgumentException
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val searchDiscoverFragment: SearchDiscoverFragment = SearchDiscoverFragment()
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
@Inject
|
||||
@ -57,6 +60,7 @@ class MainActivity : AppCompatActivity() {
|
||||
const val ADD_ACCOUNT_IDENTIFIER: Long = -13
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(R.style.AppTheme_NoActionBar)
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -75,13 +79,19 @@ class MainActivity : AppCompatActivity() {
|
||||
launchActivity(LoginActivity(), firstTime = true)
|
||||
} else {
|
||||
setupDrawer()
|
||||
val tabs = arrayOf(
|
||||
if (hasInternet(applicationContext)) HomeTimelineFragment()
|
||||
else OfflineFeedFragment(),
|
||||
searchDiscoverFragment,
|
||||
|
||||
val tabs: List<Fragment> = listOf(
|
||||
PostFeedFragment<HomeStatusDatabaseEntity>()
|
||||
.apply {
|
||||
arguments = Bundle().apply { putBoolean("home", true) }
|
||||
},
|
||||
SearchDiscoverFragment(),
|
||||
CameraFragment(),
|
||||
NotificationsFragment(),
|
||||
PublicTimelineFragment()
|
||||
PostFeedFragment<PublicFeedStatusDatabaseEntity>()
|
||||
.apply {
|
||||
arguments = Bundle().apply { putBoolean("home", false) }
|
||||
}
|
||||
)
|
||||
setupTabs(tabs)
|
||||
}
|
||||
@ -123,7 +133,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
override fun placeholder(ctx: Context, tag: String?): Drawable {
|
||||
if (tag == DrawerImageLoader.Tags.PROFILE.name || tag == DrawerImageLoader.Tags.PROFILE_DRAWER_ITEM.name) {
|
||||
return ctx.getDrawable(R.drawable.ic_default_user)!!
|
||||
return ContextCompat.getDrawable(ctx, R.drawable.ic_default_user)!!
|
||||
}
|
||||
|
||||
return super.placeholder(ctx, tag)
|
||||
@ -264,24 +274,30 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
|
||||
private fun setupTabs(tab_array: Array<Fragment>){
|
||||
private fun setupTabs(tab_array: List<Fragment>){
|
||||
view_pager.adapter = object : FragmentStateAdapter(this) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return tab_array[position]
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return 5
|
||||
return tab_array.size
|
||||
}
|
||||
}
|
||||
|
||||
//Keep the tabs active to prevent reloads and stutters
|
||||
view_pager.offscreenPageLimit = tab_array.size - 1
|
||||
|
||||
TabLayoutMediator(tabs, view_pager) { tab, position ->
|
||||
when(position){
|
||||
0 -> tab.icon = getDrawable(R.drawable.ic_home_white_24dp)
|
||||
1 -> tab.icon = getDrawable(R.drawable.ic_search_white_24dp)
|
||||
2 -> tab.icon = getDrawable(R.drawable.ic_photo_camera_white_24dp)
|
||||
3 -> tab.icon = getDrawable(R.drawable.ic_heart)
|
||||
4 -> tab.icon = getDrawable(R.drawable.ic_filter_black_24dp)
|
||||
}
|
||||
tab.icon = ContextCompat.getDrawable(applicationContext,
|
||||
when(position){
|
||||
0 -> R.drawable.ic_home_white_24dp
|
||||
1 -> R.drawable.ic_search_white_24dp
|
||||
2 -> R.drawable.ic_photo_camera_white_24dp
|
||||
3 -> R.drawable.ic_heart
|
||||
4 -> R.drawable.ic_filter_black_24dp
|
||||
else -> throw IllegalArgumentException()
|
||||
})
|
||||
}.attach()
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ import com.bumptech.glide.Glide
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.interfaces.PostCreationListener
|
||||
import com.h.pixeldroid.objects.Attachment
|
||||
|
@ -17,14 +17,13 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.h.pixeldroid.adapters.ProfilePostsRecyclerViewAdapter
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Relationship
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.HtmlUtils.Companion.parseHTMLText
|
||||
import com.h.pixeldroid.utils.ImageConverter
|
||||
import kotlinx.android.synthetic.main.fragment_search.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
@ -145,9 +144,9 @@ class ProfileActivity : AppCompatActivity() {
|
||||
if(show){
|
||||
motionLayout?.transitionToEnd()
|
||||
} else {
|
||||
findViewById<ProgressBar>(R.id.profileProgressBar).visibility = View.GONE
|
||||
motionLayout?.transitionToStart()
|
||||
}
|
||||
findViewById<ProgressBar>(R.id.profileProgressBar).visibility = View.GONE
|
||||
refreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,9 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.h.pixeldroid.fragments.feeds.search.SearchAccountFragment
|
||||
import com.h.pixeldroid.fragments.feeds.search.SearchHashtagFragment
|
||||
import com.h.pixeldroid.fragments.feeds.search.SearchPostsFragment
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.search.SearchAccountFragment
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.search.SearchHashtagFragment
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.search.SearchPostsFragment
|
||||
import com.h.pixeldroid.objects.Results
|
||||
|
||||
class SearchActivity : AppCompatActivity() {
|
||||
@ -54,8 +54,7 @@ class SearchActivity : AppCompatActivity() {
|
||||
|
||||
private fun createSearchTabs(query: String): Array<Fragment>{
|
||||
|
||||
val searchFeedFragment =
|
||||
SearchPostsFragment()
|
||||
val searchFeedFragment = SearchPostsFragment()
|
||||
val searchAccountListFragment =
|
||||
SearchAccountFragment()
|
||||
val searchHashtagFragment: Fragment = SearchHashtagFragment()
|
||||
|
@ -5,6 +5,7 @@ import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import okhttp3.MultipartBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
@ -153,16 +154,16 @@ interface PixelfedAPI {
|
||||
) : Call<Context>
|
||||
|
||||
@GET("/api/v1/timelines/public")
|
||||
fun timelinePublic(
|
||||
suspend fun timelinePublic(
|
||||
@Query("local") local: Boolean? = null,
|
||||
@Query("max_id") max_id: String? = null,
|
||||
@Query("since_id") since_id: String? = null,
|
||||
@Query("min_id") min_id: String? = null,
|
||||
@Query("limit") limit: String? = null
|
||||
): Call<List<Status>>
|
||||
): List<Status>
|
||||
|
||||
@GET("/api/v1/timelines/home")
|
||||
fun timelineHome(
|
||||
suspend fun timelineHome(
|
||||
//The authorization header needs to be of the form "Bearer <token>"
|
||||
@Header("Authorization") authorization: String,
|
||||
@Query("max_id") max_id: String? = null,
|
||||
@ -170,10 +171,10 @@ interface PixelfedAPI {
|
||||
@Query("min_id") min_id: String? = null,
|
||||
@Query("limit") limit: String? = null,
|
||||
@Query("local") local: Boolean? = null
|
||||
): Call<List<Status>>
|
||||
): List<Status>
|
||||
|
||||
@GET("/api/v2/search")
|
||||
fun search(
|
||||
suspend fun search(
|
||||
//The authorization header needs to be of the form "Bearer <token>"
|
||||
@Header("Authorization") authorization: String,
|
||||
@Query("account_id") account_id: String? = null,
|
||||
@ -186,24 +187,19 @@ interface PixelfedAPI {
|
||||
@Query("limit") limit: String? = null,
|
||||
@Query("offset") offset: String? = null,
|
||||
@Query("following") following: Boolean? = null
|
||||
): Call<Results>
|
||||
): Results
|
||||
|
||||
/*
|
||||
Note: as of 0.10.8, Pixelfed does not seem to respect the Mastodon API documentation,
|
||||
you *need* to pass one of the so-called "optional" arguments. See:
|
||||
https://github.com/pixelfed/pixelfed/blob/dev/app/Http/Controllers/Api/ApiV1Controller.php
|
||||
An example that works: specify min_id as 1 (not 0 though)
|
||||
*/
|
||||
@GET("/api/v1/notifications")
|
||||
fun notifications(
|
||||
suspend fun notifications(
|
||||
//The authorization header needs to be of the form "Bearer <token>"
|
||||
@Header("Authorization") authorization: String,
|
||||
@Query("max_id") max_id: String? = null,
|
||||
@Query("since_id") since_id: String? = null,
|
||||
@Query("min_id") min_id: String? = null,
|
||||
@Query("exclude_types") limit: String? = null,
|
||||
@Query("account_id") exclude_types: Boolean? = null
|
||||
): Call<List<Notification>>
|
||||
@Query("limit") limit: String? = null,
|
||||
@Query("exclude_types") exclude_types: List<String>? = null,
|
||||
@Query("account_id") account_id: Boolean? = null
|
||||
): List<Notification>
|
||||
|
||||
@GET("/api/v1/accounts/verify_credentials")
|
||||
fun verifyCredentials(
|
||||
@ -224,24 +220,24 @@ interface PixelfedAPI {
|
||||
) : Call<List<Relationship>>
|
||||
|
||||
@GET("/api/v1/accounts/{id}/followers")
|
||||
fun followers(
|
||||
suspend fun followers(
|
||||
@Path("id") account_id: String,
|
||||
@Header("Authorization") authorization: String,
|
||||
@Query("max_id") max_id: String? = null,
|
||||
@Query("since_id") since_id: String? = null,
|
||||
@Query("limit") limit: Number? = null,
|
||||
@Query("page") page: String? = null
|
||||
) : Call<List<Account>>
|
||||
) : Response<List<Account>>
|
||||
|
||||
@GET("/api/v1/accounts/{id}/following")
|
||||
fun following(
|
||||
suspend fun following(
|
||||
@Path("id") account_id: String,
|
||||
@Header("Authorization") authorization: String,
|
||||
@Query("max_id") max_id: String? = null,
|
||||
@Query("since_id") since_id: String? = null,
|
||||
@Query("limit") limit: Number? = 40,
|
||||
@Query("page") page: String? = null
|
||||
) : Call<List<Account>>
|
||||
) : Response<List<Account>>
|
||||
|
||||
@GET("/api/v1/accounts/{id}")
|
||||
fun getAccount(
|
||||
|
@ -3,11 +3,22 @@ package com.h.pixeldroid.db
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.h.pixeldroid.db.dao.*
|
||||
import com.h.pixeldroid.db.dao.feedContent.NotificationDao
|
||||
import com.h.pixeldroid.db.dao.feedContent.posts.HomePostDao
|
||||
import com.h.pixeldroid.db.dao.feedContent.posts.PublicPostDao
|
||||
import com.h.pixeldroid.db.entities.HomeStatusDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.objects.Notification
|
||||
|
||||
@Database(entities = [
|
||||
InstanceDatabaseEntity::class,
|
||||
UserDatabaseEntity::class,
|
||||
PostDatabaseEntity::class
|
||||
HomeStatusDatabaseEntity::class,
|
||||
PublicFeedStatusDatabaseEntity::class,
|
||||
Notification::class
|
||||
],
|
||||
version = 1
|
||||
)
|
||||
@ -15,5 +26,7 @@ import androidx.room.TypeConverters
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun instanceDao(): InstanceDao
|
||||
abstract fun userDao(): UserDao
|
||||
abstract fun postDao(): PostDao
|
||||
abstract fun homePostDao(): HomePostDao
|
||||
abstract fun publicPostDao(): PublicPostDao
|
||||
abstract fun notificationDao(): NotificationDao
|
||||
}
|
@ -2,7 +2,9 @@ package com.h.pixeldroid.db
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
import java.util.Date
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.h.pixeldroid.objects.*
|
||||
import java.util.*
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
@ -16,7 +18,113 @@ class Converters {
|
||||
fun dateToJson(date: Date): String = Gson().toJson(date)
|
||||
|
||||
@TypeConverter
|
||||
fun jsontoDate(json: String): Date = Gson().fromJson(json, Date::class.java)
|
||||
fun jsonToDate(json: String): Date = Gson().fromJson(json, Date::class.java)
|
||||
|
||||
@TypeConverter
|
||||
fun accountToJson(account: Account): String = Gson().toJson(account)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccount(json: String): Account = Gson().fromJson(json, Account::class.java)
|
||||
|
||||
@TypeConverter
|
||||
fun statusToJson(status: Status?): String = Gson().toJson(status)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToStatus(json: String): Status? = Gson().fromJson(json, Status::class.java)
|
||||
|
||||
@TypeConverter
|
||||
fun notificationTypeToJson(type: Notification.NotificationType?): String = Gson().toJson(type)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToNotificationType(json: String): Notification.NotificationType? = Gson().fromJson(
|
||||
json,
|
||||
Notification.NotificationType::class.java
|
||||
)
|
||||
|
||||
@TypeConverter
|
||||
fun applicationToJson(type: Application?): String = Gson().toJson(type)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToApplication(json: String): Application? = Gson().fromJson(
|
||||
json,
|
||||
Application::class.java
|
||||
)
|
||||
|
||||
@TypeConverter
|
||||
fun cardToJson(type: Card?): String = Gson().toJson(type)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToCard(json: String): Card? = Gson().fromJson(json, Card::class.java)
|
||||
|
||||
@TypeConverter
|
||||
fun attachmentToJson(type: Attachment?): String = Gson().toJson(type)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAttachment(json: String): Attachment? = Gson().fromJson(json, Attachment::class.java)
|
||||
|
||||
@TypeConverter
|
||||
fun attachmentListToJson(type: List<Attachment>?): String {
|
||||
val listType = object : TypeToken<List<Attachment?>?>() {}.type
|
||||
return Gson().toJson(type, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAttachmentList(json: String): List<Attachment>? {
|
||||
val listType = object : TypeToken<List<Attachment?>?>() {}.type
|
||||
return Gson().fromJson(json, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun mentionListToJson(type: List<Mention>?): String {
|
||||
val listType = object : TypeToken<List<Mention?>?>() {}.type
|
||||
return Gson().toJson(type, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToMentionList(json: String): List<Mention>? {
|
||||
val listType = object : TypeToken<List<Mention?>?>() {}.type
|
||||
return Gson().fromJson(json, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun emojiListToJson(type: List<Emoji>?): String {
|
||||
val listType = object : TypeToken<List<Emoji?>?>() {}.type
|
||||
return Gson().toJson(type, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToEmojiList(json: String): List<Emoji>? {
|
||||
val listType = object : TypeToken<List<Emoji?>?>() {}.type
|
||||
return Gson().fromJson(json, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun tagListToJson(type: List<Tag>?): String {
|
||||
val listType = object : TypeToken<List<Tag?>?>() {}.type
|
||||
return Gson().toJson(type, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToTagList(json: String): List<Tag>? {
|
||||
val listType = object : TypeToken<List<Tag?>?>() {}.type
|
||||
return Gson().fromJson(json, listType)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun pollToJson(type: Poll?): String = Gson().toJson(type)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToPoll(json: String): Poll? = Gson().fromJson(json, Poll::class.java)
|
||||
|
||||
@TypeConverter
|
||||
fun visibilityToJson(type: Status.Visibility?): String = Gson().toJson(type)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToVisibility(json: String): Status.Visibility? = Gson().fromJson(
|
||||
json,
|
||||
Status.Visibility::class.java
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package com.h.pixeldroid.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface PostDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertPost(post: PostDatabaseEntity)
|
||||
|
||||
@Query("SELECT COUNT(*) FROM posts WHERE user_id=:userId AND instance_uri=:instanceUri")
|
||||
fun numberOfPosts(userId: String, instanceUri: String): Int
|
||||
|
||||
@Query("SELECT * FROM posts WHERE user_id=:user AND instance_uri=:instanceUri ORDER BY date DESC")
|
||||
fun getAll(user: String, instanceUri: String): List<PostDatabaseEntity>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM posts WHERE uri=:uri AND user_id=:userId AND instance_uri=:instanceUri")
|
||||
fun count(uri: String, userId: String, instanceUri: String): Int
|
||||
|
||||
@Query(
|
||||
"""DELETE FROM posts WHERE uri IN (
|
||||
SELECT uri FROM posts ORDER BY date ASC LIMIT :nPosts
|
||||
)"""
|
||||
)
|
||||
fun removeOlderPosts(nPosts: Int)
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package com.h.pixeldroid.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import java.util.Date
|
||||
|
||||
@Entity(
|
||||
tableName = "posts",
|
||||
primaryKeys = ["uri", "user_id", "instance_uri"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserDatabaseEntity::class,
|
||||
parentColumns = arrayOf("user_id", "instance_uri"),
|
||||
childColumns = arrayOf("user_id", "instance_uri"),
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["user_id"])]
|
||||
)
|
||||
data class PostDatabaseEntity (
|
||||
var uri: String,
|
||||
var user_id: String,
|
||||
var instance_uri: String,
|
||||
var account_profile_picture: String,
|
||||
var account_name: String,
|
||||
var media_urls: List<String>,
|
||||
var favourite_count: Int,
|
||||
var reply_count: Int,
|
||||
var share_count: Int,
|
||||
var description: String,
|
||||
var date: Date,
|
||||
var likes: Int,
|
||||
var shares: Int
|
||||
)
|
@ -1,9 +1,10 @@
|
||||
package com.h.pixeldroid.db
|
||||
package com.h.pixeldroid.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface InstanceDao {
|
@ -1,9 +1,10 @@
|
||||
package com.h.pixeldroid.db
|
||||
package com.h.pixeldroid.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface UserDao {
|
@ -0,0 +1,17 @@
|
||||
package com.h.pixeldroid.db.dao.feedContent
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import com.h.pixeldroid.objects.FeedContentDatabase
|
||||
|
||||
interface FeedContentDao<T: FeedContentDatabase>{
|
||||
|
||||
fun feedContent(userId: String, instanceUri: String): PagingSource<Int, T>
|
||||
|
||||
suspend fun clearFeedContent()
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAll(feedContent: List<T>)
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.h.pixeldroid.db.dao.feedContent
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.h.pixeldroid.objects.Notification
|
||||
|
||||
@Dao
|
||||
interface NotificationDao: FeedContentDao<Notification> {
|
||||
|
||||
@Query("DELETE FROM notifications")
|
||||
override suspend fun clearFeedContent()
|
||||
|
||||
@Query("""SELECT * FROM notifications WHERE user_id=:userId AND instance_uri=:instanceUri
|
||||
ORDER BY CAST(created_at AS FLOAT) DESC""")
|
||||
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, Notification>
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.h.pixeldroid.db.dao.feedContent.posts
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.h.pixeldroid.db.dao.feedContent.FeedContentDao
|
||||
import com.h.pixeldroid.db.entities.HomeStatusDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface HomePostDao: FeedContentDao<HomeStatusDatabaseEntity> {
|
||||
@Query("""SELECT * FROM homePosts WHERE user_id=:userId AND instance_uri=:instanceUri
|
||||
ORDER BY CAST(created_at AS FLOAT)""")
|
||||
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, HomeStatusDatabaseEntity>
|
||||
|
||||
@Query("DELETE FROM homePosts")
|
||||
override suspend fun clearFeedContent()
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.h.pixeldroid.db.dao.feedContent.posts
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.h.pixeldroid.db.dao.feedContent.FeedContentDao
|
||||
import com.h.pixeldroid.db.entities.PublicFeedStatusDatabaseEntity
|
||||
|
||||
@Dao
|
||||
interface PublicPostDao: FeedContentDao<PublicFeedStatusDatabaseEntity> {
|
||||
@Query("""SELECT * FROM publicPosts WHERE user_id=:userId AND instance_uri=:instanceUri
|
||||
ORDER BY CAST(created_at AS FLOAT)""")
|
||||
override fun feedContent(userId: String, instanceUri: String): PagingSource<Int, PublicFeedStatusDatabaseEntity>
|
||||
|
||||
@Query("DELETE FROM publicPosts")
|
||||
override suspend fun clearFeedContent()
|
||||
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package com.h.pixeldroid.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import com.h.pixeldroid.objects.*
|
||||
import java.util.*
|
||||
|
||||
@Entity(
|
||||
tableName = "homePosts",
|
||||
primaryKeys = ["id", "user_id", "instance_uri"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserDatabaseEntity::class,
|
||||
parentColumns = arrayOf("user_id", "instance_uri"),
|
||||
childColumns = arrayOf("user_id", "instance_uri"),
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["user_id", "instance_uri"])]
|
||||
)
|
||||
class HomeStatusDatabaseEntity(
|
||||
override var user_id: String,
|
||||
override var instance_uri: String,
|
||||
status: Status
|
||||
): Status(
|
||||
status.id,
|
||||
status.uri,
|
||||
status.created_at,
|
||||
status.account,
|
||||
status.content,
|
||||
status.visibility,
|
||||
status.sensitive,
|
||||
status.spoiler_text,
|
||||
status.media_attachments,
|
||||
status.application,
|
||||
status.mentions,
|
||||
status.tags,
|
||||
status.emojis,
|
||||
status.reblogs_count,
|
||||
status.favourites_count,
|
||||
status.replies_count,
|
||||
status.url,
|
||||
status.in_reply_to_id,
|
||||
status.in_reply_to_account,
|
||||
status.reblog,
|
||||
status.poll,
|
||||
status.card,
|
||||
status.language,
|
||||
status.text,
|
||||
status.favourited,
|
||||
status.reblogged,
|
||||
status.muted,
|
||||
status.bookmarked,
|
||||
status.pinned
|
||||
), FeedContentDatabase {
|
||||
//Constructor to make Room happy. This sucks, and I know it.
|
||||
constructor(id: String,
|
||||
uri: String? = "",
|
||||
created_at: Date? = Date(0),
|
||||
account: Account?,
|
||||
content: String? = "",
|
||||
visibility: Visibility? = Visibility.public,
|
||||
sensitive: Boolean? = false,
|
||||
spoiler_text: String? = "",
|
||||
media_attachments: List<Attachment>? = null,
|
||||
application: Application? = null,
|
||||
|
||||
mentions: List<Mention>? = null,
|
||||
tags: List<Tag>? = null,
|
||||
emojis: List<Emoji>? = null,
|
||||
|
||||
reblogs_count: Int? = 0,
|
||||
favourites_count: Int? = 0,
|
||||
replies_count: Int? = 0,
|
||||
|
||||
url: String? = null,
|
||||
in_reply_to_id: String? = null,
|
||||
in_reply_to_account: String? = null,
|
||||
reblog: Status? = null,
|
||||
poll: Poll? = null,
|
||||
card: Card? = null,
|
||||
language: String? = null,
|
||||
text: String? = null,
|
||||
|
||||
favourited: Boolean? = false,
|
||||
reblogged: Boolean? = false,
|
||||
muted: Boolean? = false,
|
||||
bookmarked: Boolean? = false,
|
||||
pinned: Boolean? = false,
|
||||
user_id: String,
|
||||
instance_uri: String): this(user_id, instance_uri, Status(id, uri, created_at, account, content, visibility, sensitive, spoiler_text, media_attachments, application, mentions, tags, emojis, reblogs_count, favourites_count, replies_count, url, in_reply_to_id, in_reply_to_account, reblog, poll, card, language, text, favourited, reblogged, muted, bookmarked, pinned)
|
||||
)
|
||||
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package com.h.pixeldroid.db
|
||||
package com.h.pixeldroid.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
@ -0,0 +1,94 @@
|
||||
package com.h.pixeldroid.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import com.h.pixeldroid.objects.*
|
||||
import java.util.*
|
||||
|
||||
@Entity(
|
||||
tableName = "publicPosts",
|
||||
primaryKeys = ["id", "user_id", "instance_uri"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserDatabaseEntity::class,
|
||||
parentColumns = arrayOf("user_id", "instance_uri"),
|
||||
childColumns = arrayOf("user_id", "instance_uri"),
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["user_id", "instance_uri"])]
|
||||
)
|
||||
class PublicFeedStatusDatabaseEntity(
|
||||
override var user_id: String,
|
||||
override var instance_uri: String,
|
||||
status: Status
|
||||
): Status(
|
||||
status.id,
|
||||
status.uri,
|
||||
status.created_at,
|
||||
status.account,
|
||||
status.content,
|
||||
status.visibility,
|
||||
status.sensitive,
|
||||
status.spoiler_text,
|
||||
status.media_attachments,
|
||||
status.application,
|
||||
status.mentions,
|
||||
status.tags,
|
||||
status.emojis,
|
||||
status.reblogs_count,
|
||||
status.favourites_count,
|
||||
status.replies_count,
|
||||
status.url,
|
||||
status.in_reply_to_id,
|
||||
status.in_reply_to_account,
|
||||
status.reblog,
|
||||
status.poll,
|
||||
status.card,
|
||||
status.language,
|
||||
status.text,
|
||||
status.favourited,
|
||||
status.reblogged,
|
||||
status.muted,
|
||||
status.bookmarked,
|
||||
status.pinned
|
||||
), FeedContentDatabase {
|
||||
//Constructor to make Room happy. This sucks, and I know it.
|
||||
constructor(id: String,
|
||||
uri: String? = "",
|
||||
created_at: Date? = Date(0),
|
||||
account: Account?,
|
||||
content: String? = "",
|
||||
visibility: Visibility? = Visibility.public,
|
||||
sensitive: Boolean? = false,
|
||||
spoiler_text: String? = "",
|
||||
media_attachments: List<Attachment>? = null,
|
||||
application: Application? = null,
|
||||
|
||||
mentions: List<Mention>? = null,
|
||||
tags: List<Tag>? = null,
|
||||
emojis: List<Emoji>? = null,
|
||||
|
||||
reblogs_count: Int? = 0,
|
||||
favourites_count: Int? = 0,
|
||||
replies_count: Int? = 0,
|
||||
|
||||
url: String? = null,
|
||||
in_reply_to_id: String? = null,
|
||||
in_reply_to_account: String? = null,
|
||||
reblog: Status? = null,
|
||||
poll: Poll? = null,
|
||||
card: Card? = null,
|
||||
language: String? = null,
|
||||
text: String? = null,
|
||||
|
||||
favourited: Boolean? = false,
|
||||
reblogged: Boolean? = false,
|
||||
muted: Boolean? = false,
|
||||
bookmarked: Boolean? = false,
|
||||
pinned: Boolean? = false,
|
||||
user_id: String,
|
||||
instance_uri: String): this(user_id, instance_uri, Status(id, uri, created_at, account, content, visibility, sensitive, spoiler_text, media_attachments, application, mentions, tags, emojis, reblogs_count, favourites_count, replies_count, url, in_reply_to_id, in_reply_to_account, reblog, poll, card, language, text, favourited, reblogged, muted, bookmarked, pinned)
|
||||
)
|
||||
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
package com.h.pixeldroid.db
|
||||
package com.h.pixeldroid.db.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "users",
|
@ -4,10 +4,10 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import com.h.pixeldroid.*
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.fragments.BaseFragment
|
||||
import com.h.pixeldroid.fragments.PostFragment
|
||||
import com.h.pixeldroid.fragments.SearchDiscoverFragment
|
||||
import com.h.pixeldroid.fragments.feeds.FeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.OfflineFeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.notifications.NotificationsFragment
|
||||
import dagger.Component
|
||||
|
||||
import javax.inject.Singleton
|
||||
@ -18,8 +18,6 @@ import javax.inject.Singleton
|
||||
interface ApplicationComponent {
|
||||
fun inject(application: Pixeldroid?)
|
||||
fun inject(activity: LoginActivity?)
|
||||
fun inject(feedFragment: FeedFragment)
|
||||
fun inject(activity: FollowsActivity?)
|
||||
fun inject(activity: PostActivity?)
|
||||
fun inject(activity: PostCreationActivity?)
|
||||
fun inject(activity: ProfileActivity?)
|
||||
@ -27,7 +25,11 @@ interface ApplicationComponent {
|
||||
fun inject(activity: ReportActivity?)
|
||||
fun inject(fragment: PostFragment)
|
||||
fun inject(fragment: SearchDiscoverFragment)
|
||||
fun inject(fragment: OfflineFeedFragment)
|
||||
|
||||
fun inject(fragment: NotificationsFragment)
|
||||
fun inject(feedFragment: BaseFragment)
|
||||
fun inject(followsActivity: FollowsActivity)
|
||||
|
||||
|
||||
val context: Context?
|
||||
val application: Application?
|
||||
|
26
app/src/main/java/com/h/pixeldroid/fragments/BaseFragment.kt
Normal file
26
app/src/main/java/com/h/pixeldroid/fragments/BaseFragment.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package com.h.pixeldroid.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.h.pixeldroid.Pixeldroid
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Base Fragment, for dependency injection and other things common to a lot of the fragments
|
||||
*/
|
||||
open class BaseFragment: Fragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
(requireActivity().application as Pixeldroid).getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package com.h.pixeldroid.fragments
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.h.pixeldroid.R
|
||||
import kotlinx.android.synthetic.main.fragment_image.*
|
||||
|
||||
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
|
||||
private const val IMG_URL = "imgurl"
|
||||
private const val IMG_DESCRIPTION = "imgdescription"
|
||||
private const val RQST_BLDR = "rqstbldr"
|
||||
|
||||
/**
|
||||
* A simple [Fragment] subclass.
|
||||
* Use the [ImageFragment.newInstance] factory method to
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
class ImageFragment : Fragment() {
|
||||
private lateinit var imgUrl: String
|
||||
private lateinit var imgDescription: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
imgUrl = it.getString(IMG_URL)!!
|
||||
imgDescription = it.getString(IMG_DESCRIPTION)!!.ifEmpty { getString(R.string.no_description) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_image, container, false)
|
||||
|
||||
view.findViewById<ImageView>(R.id.imageImageView).setOnLongClickListener {
|
||||
Snackbar.make(it, imgDescription, Snackbar.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
//Load the image into to view
|
||||
Glide.with(this)
|
||||
.asDrawable().fitCenter()
|
||||
.placeholder(ColorDrawable(Color.GRAY))
|
||||
.load(imgUrl)
|
||||
.into(view.findViewById(R.id.imageImageView)!!)
|
||||
imageImageView.contentDescription = imgDescription
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param imageUrl the url of the image we want to create a fragment for
|
||||
* @return A new instance of fragment ImageFragment.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newInstance(imageUrl: String, imageDescription: String) =
|
||||
ImageFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(IMG_URL, imageUrl)
|
||||
putString(IMG_DESCRIPTION, imageDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +1,42 @@
|
||||
package com.h.pixeldroid.fragments
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.bumptech.glide.Glide
|
||||
import com.h.pixeldroid.Pixeldroid
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.PostViewHolder
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.objects.Status.Companion.DOMAIN_TAG
|
||||
import com.h.pixeldroid.objects.Status.Companion.POST_TAG
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class PostFragment : Fragment() {
|
||||
class PostFragment : BaseFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
private lateinit var statusDomain: String
|
||||
private var currentStatus: Status? = null
|
||||
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
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? {
|
||||
val currentStatus = arguments?.getSerializable(POST_TAG) as Status?
|
||||
val statusDomain = arguments?.getString(DOMAIN_TAG)!!
|
||||
val root: View = inflater.inflate(R.layout.post_fragment, container, false)
|
||||
val picRequest = Glide.with(this)
|
||||
.asDrawable().fitCenter()
|
||||
.placeholder(ColorDrawable(Color.GRAY))
|
||||
|
||||
(requireActivity().application as Pixeldroid).getAppComponent().inject(this)
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
|
||||
val user = db.userDao().getActiveUser()
|
||||
val accessToken = user.accessToken
|
||||
val api = apiHolder.api ?: apiHolder.setDomain(user.instance_uri)
|
||||
|
||||
val accessToken = user?.accessToken.orEmpty()
|
||||
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
|
||||
val holder = StatusViewHolder(root)
|
||||
|
||||
currentStatus?.setupPost(root, picRequest, this, statusDomain, true)
|
||||
|
||||
val holder = PostViewHolder(
|
||||
root,
|
||||
root.context
|
||||
)
|
||||
|
||||
currentStatus?.activateButtons(holder, api, "Bearer $accessToken")
|
||||
holder.bind(currentStatus, statusDomain, api, "Bearer $accessToken")
|
||||
|
||||
return root
|
||||
|
||||
|
731
app/src/main/java/com/h/pixeldroid/fragments/StatusViewHolder.kt
Normal file
731
app/src/main/java/com/h/pixeldroid/fragments/StatusViewHolder.kt
Normal file
@ -0,0 +1,731 @@
|
||||
package com.h.pixeldroid.fragments
|
||||
|
||||
import android.Manifest
|
||||
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
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import at.connyduck.sparkbutton.SparkButton
|
||||
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.ReportActivity
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.objects.Attachment
|
||||
import com.h.pixeldroid.objects.Context
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.HtmlUtils
|
||||
import com.h.pixeldroid.utils.ImageConverter
|
||||
import com.h.pixeldroid.utils.Utils
|
||||
import com.karumi.dexter.Dexter
|
||||
import com.karumi.dexter.listener.PermissionDeniedResponse
|
||||
import com.karumi.dexter.listener.PermissionGrantedResponse
|
||||
import com.karumi.dexter.listener.single.BasePermissionListener
|
||||
import kotlinx.android.synthetic.main.comment.view.*
|
||||
import kotlinx.android.synthetic.main.post_fragment.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
|
||||
/**
|
||||
* View Holder for a [Status] RecyclerView list item.
|
||||
*/
|
||||
class StatusViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
|
||||
val profilePic : ImageView = view.findViewById(R.id.profilePic)
|
||||
val postPic : ImageView = view.findViewById(R.id.postPicture)
|
||||
val username : TextView = view.findViewById(R.id.username)
|
||||
val usernameDesc: TextView = view.findViewById(R.id.usernameDesc)
|
||||
val description : TextView = view.findViewById(R.id.description)
|
||||
val nlikes : TextView = view.findViewById(R.id.nlikes)
|
||||
val nshares : TextView = view.findViewById(R.id.nshares)
|
||||
|
||||
//Spark buttons
|
||||
val liker : SparkButton = view.findViewById(R.id.liker)
|
||||
val reblogger : SparkButton = view.findViewById(R.id.reblogger)
|
||||
|
||||
val submitCmnt : ImageButton = view.findViewById(R.id.submitComment)
|
||||
val commenter : ImageView = view.findViewById(R.id.commenter)
|
||||
val comment : EditText = view.findViewById(R.id.editComment)
|
||||
val commentCont : LinearLayout = view.findViewById(R.id.commentContainer)
|
||||
val commentIn : LinearLayout = view.findViewById(R.id.commentIn)
|
||||
val viewComment : TextView = view.findViewById(R.id.ViewComments)
|
||||
val postDate : TextView = view.findViewById(R.id.postDate)
|
||||
val postDomain : TextView = view.findViewById(R.id.postDomain)
|
||||
val sensitiveW : TextView = view.findViewById(R.id.sensitiveWarning)
|
||||
val postPager : ViewPager2 = view.findViewById(R.id.postPager)
|
||||
val more : ImageButton = view.findViewById(R.id.status_more)
|
||||
|
||||
private var status: Status? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
//notification?.openActivity()
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(status: Status?, instanceUri: String, pixelfedAPI: PixelfedAPI, credential: String) {
|
||||
|
||||
this.status = status
|
||||
|
||||
val metrics = itemView.context.resources.displayMetrics
|
||||
//Limit the height of the different images
|
||||
postPic.maxHeight = metrics.heightPixels * 3/4
|
||||
|
||||
//Setup the post layout
|
||||
val picRequest = Glide.with(itemView)
|
||||
.asDrawable().fitCenter()
|
||||
.placeholder(ColorDrawable(Color.GRAY))
|
||||
|
||||
setupPost(itemView, picRequest, instanceUri, false)
|
||||
|
||||
activateButtons(this, pixelfedAPI, credential)
|
||||
|
||||
}
|
||||
|
||||
fun setupPost(
|
||||
rootView: View,
|
||||
request: RequestBuilder<Drawable>,
|
||||
//homeFragment: Fragment,
|
||||
domain: String,
|
||||
isActivity: Boolean
|
||||
) {
|
||||
//Setup username as a button that opens the profile
|
||||
rootView.findViewById<TextView>(R.id.username).apply {
|
||||
text = status?.account?.getDisplayName() ?: ""
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
setOnClickListener { status?.account?.openProfile(rootView.context) }
|
||||
}
|
||||
|
||||
rootView.findViewById<TextView>(R.id.usernameDesc).apply {
|
||||
text = status?.account?.getDisplayName() ?: ""
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
|
||||
rootView.findViewById<TextView>(R.id.nlikes).apply {
|
||||
text = status?.getNLikes(rootView.context)
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
|
||||
rootView.findViewById<TextView>(R.id.nshares).apply {
|
||||
text = status?.getNShares(rootView.context)
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
|
||||
//Convert the date to a readable string
|
||||
Utils.setTextViewFromISO8601(
|
||||
status?.created_at!!,
|
||||
rootView.postDate,
|
||||
isActivity,
|
||||
rootView.context
|
||||
)
|
||||
|
||||
rootView.postDomain.text = status?.getStatusDomain(domain)
|
||||
|
||||
//Setup images
|
||||
ImageConverter.setRoundImageFromURL(
|
||||
rootView,
|
||||
status?.getProfilePicUrl(),
|
||||
rootView.profilePic
|
||||
)
|
||||
rootView.profilePic.setOnClickListener { status?.account?.openProfile(rootView.context) }
|
||||
|
||||
//Setup post pic only if there are media attachments
|
||||
if(!status?.media_attachments.isNullOrEmpty()) {
|
||||
setupPostPics(rootView, request)
|
||||
} else {
|
||||
rootView.postPicture.visibility = View.GONE
|
||||
rootView.postPager.visibility = View.GONE
|
||||
rootView.postTabs.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
//Set comment initial visibility
|
||||
rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = View.GONE
|
||||
rootView.findViewById<LinearLayout>(R.id.commentContainer).visibility = View.GONE
|
||||
}
|
||||
|
||||
fun setupPostPics(
|
||||
rootView: View,
|
||||
request: RequestBuilder<Drawable>,
|
||||
//homeFragment: Fragment
|
||||
) {
|
||||
|
||||
// Standard layout
|
||||
rootView.postPicture.visibility = View.VISIBLE
|
||||
rootView.postPager.visibility = View.GONE
|
||||
rootView.postTabs.visibility = View.GONE
|
||||
|
||||
|
||||
if(status?.media_attachments?.size == 1) {
|
||||
request.load(status?.getPostUrl()).into(rootView.postPicture)
|
||||
val imgDescription = status?.media_attachments?.get(0)?.description.orEmpty().ifEmpty { rootView.context.getString(
|
||||
R.string.no_description) }
|
||||
rootView.postPicture.contentDescription = imgDescription
|
||||
|
||||
rootView.postPicture.setOnLongClickListener {
|
||||
Snackbar.make(it, imgDescription, Snackbar.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
|
||||
} else if(status?.media_attachments?.size!! > 1) {
|
||||
setupTabsLayout(rootView, request)
|
||||
}
|
||||
|
||||
if (status?.sensitive!!) {
|
||||
status?.setupSensitiveLayout(rootView)
|
||||
}
|
||||
}
|
||||
|
||||
fun setupTabsLayout(
|
||||
rootView: View,
|
||||
request: RequestBuilder<Drawable>,
|
||||
) {
|
||||
//Only show the viewPager and tabs
|
||||
rootView.postPicture.visibility = View.GONE
|
||||
rootView.postPager.visibility = View.VISIBLE
|
||||
rootView.postTabs.visibility = View.VISIBLE
|
||||
|
||||
//Attach the given tabs to the view pager
|
||||
rootView.postPager.adapter = AlbumViewPagerAdapter(status?.media_attachments ?: emptyList())
|
||||
|
||||
TabLayoutMediator(rootView.postTabs, rootView.postPager) { tab, _ ->
|
||||
tab.icon = ContextCompat.getDrawable(rootView.context, R.drawable.ic_dot_blue_12dp)
|
||||
}.attach()
|
||||
}
|
||||
|
||||
fun setDescription(rootView: View, api: PixelfedAPI, credential: String) {
|
||||
rootView.findViewById<TextView>(R.id.description).apply {
|
||||
if (status?.content.isNullOrBlank()) {
|
||||
visibility = View.GONE
|
||||
} else {
|
||||
text = HtmlUtils.parseHTMLText(status?.content.orEmpty(), status?.mentions, api, rootView.context, credential)
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun activateButtons(holder: StatusViewHolder, api: PixelfedAPI, credential: String){
|
||||
|
||||
//Set the special HTML text
|
||||
setDescription(holder.view, api, credential)
|
||||
|
||||
//Activate onclickListeners
|
||||
activateLiker(
|
||||
holder, api, credential,
|
||||
status?.favourited ?: false
|
||||
)
|
||||
activateReblogger(
|
||||
holder, api, credential,
|
||||
status?.reblogged ?: false
|
||||
)
|
||||
activateCommenter(holder, api, credential)
|
||||
|
||||
showComments(holder, api, credential)
|
||||
|
||||
//Activate double tap liking
|
||||
activateDoubleTapLiker(holder, api, credential)
|
||||
|
||||
activateMoreButton(holder)
|
||||
}
|
||||
|
||||
fun activateReblogger(
|
||||
holder: StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
isReblogged: Boolean
|
||||
) {
|
||||
holder.reblogger.apply {
|
||||
//Set initial button state
|
||||
isChecked = isReblogged
|
||||
|
||||
//Activate the button
|
||||
setEventListener { _, buttonState ->
|
||||
if (buttonState) {
|
||||
// Button is active
|
||||
undoReblogPost(holder, api, credential)
|
||||
} else {
|
||||
// Button is inactive
|
||||
reblogPost(holder, api, credential)
|
||||
}
|
||||
//show animation or not?
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reblogPost(
|
||||
holder : StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String
|
||||
) {
|
||||
//Call the api function
|
||||
status?.id?.let {
|
||||
api.reblogStatus(credential, it).enqueue(object : Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("REBLOG ERROR", t.toString())
|
||||
holder.reblogger.isChecked = false
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
|
||||
//Update shown share count
|
||||
holder.nshares.text = resp.getNShares(holder.view.context)
|
||||
holder.reblogger.isChecked = resp.reblogged!!
|
||||
} else {
|
||||
Log.e("RESPONSE_CODE", response.code().toString())
|
||||
holder.reblogger.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun undoReblogPost(
|
||||
holder : StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
) {
|
||||
//Call the api function
|
||||
status?.id?.let {
|
||||
api.undoReblogStatus(credential, it).enqueue(object : Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("REBLOG ERROR", t.toString())
|
||||
holder.reblogger.isChecked = true
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
|
||||
//Update shown share count
|
||||
holder.nshares.text = resp.getNShares(holder.view.context)
|
||||
holder.reblogger.isChecked = resp.reblogged!!
|
||||
} else {
|
||||
Log.e("RESPONSE_CODE", response.code().toString())
|
||||
holder.reblogger.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun activateMoreButton(holder: StatusViewHolder){
|
||||
holder.more.setOnClickListener {
|
||||
PopupMenu(it.context, it).apply {
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.post_more_menu_report -> {
|
||||
val intent = Intent(it.context, ReportActivity::class.java)
|
||||
intent.putExtra(Status.POST_TAG, status)
|
||||
ContextCompat.startActivity(it.context, intent, null)
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_share_link -> {
|
||||
val share = Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, status?.uri)
|
||||
|
||||
type = "text/plain"
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, status?.content)
|
||||
}, null)
|
||||
ContextCompat.startActivity(it.context, share, null)
|
||||
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_save_to_gallery -> {
|
||||
Dexter.withContext(holder.view.context)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.withListener(object : BasePermissionListener() {
|
||||
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
|
||||
Toast.makeText(
|
||||
holder.view.context,
|
||||
holder.view.context.getString(R.string.write_permission_download_pic),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
|
||||
status?.downloadImage(
|
||||
holder.view.context,
|
||||
status?.media_attachments?.get(holder.postPager.currentItem)?.url
|
||||
?: "",
|
||||
holder.view
|
||||
)
|
||||
}
|
||||
}).check()
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_share_picture -> {
|
||||
Dexter.withContext(holder.view.context)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.withListener(object : BasePermissionListener() {
|
||||
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
|
||||
Toast.makeText(
|
||||
holder.view.context,
|
||||
holder.view.context.getString(R.string.write_permission_share_pic),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
|
||||
status?.downloadImage(
|
||||
holder.view.context,
|
||||
status?.media_attachments?.get(holder.postPager.currentItem)?.url
|
||||
?: "",
|
||||
holder.view,
|
||||
share = true,
|
||||
)
|
||||
}
|
||||
}).check()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
inflate(R.menu.post_more_menu)
|
||||
if(status?.media_attachments.isNullOrEmpty()) {
|
||||
//make sure to disable image-related things if there aren't any
|
||||
menu.setGroupVisible(R.id.post_more_group_picture, false)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun activateDoubleTapLiker(
|
||||
holder: StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String
|
||||
) {
|
||||
holder.apply {
|
||||
var clicked = false
|
||||
postPic.setOnClickListener {
|
||||
//Check that the post isn't hidden
|
||||
if(sensitiveW.visibility == View.GONE) {
|
||||
//Check for double click
|
||||
if(clicked) {
|
||||
if (holder.liker.isChecked) {
|
||||
// Button is active, unlike
|
||||
holder.liker.isChecked = false
|
||||
unLikePostCall(holder, api, credential)
|
||||
} else {
|
||||
// Button is inactive, like
|
||||
holder.liker.playAnimation()
|
||||
holder.liker.isChecked = true
|
||||
likePostCall(holder, api, credential)
|
||||
}
|
||||
} else {
|
||||
clicked = true
|
||||
|
||||
//Reset clicked to false after 500ms
|
||||
postPic.handler.postDelayed(fun() { clicked = false }, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun activateLiker(
|
||||
holder: StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
isLiked: Boolean
|
||||
) {
|
||||
|
||||
holder.liker.apply {
|
||||
//Set initial state
|
||||
isChecked = isLiked
|
||||
|
||||
//Activate the liker
|
||||
setEventListener { _, buttonState ->
|
||||
if (buttonState) {
|
||||
// Button is active, unlike
|
||||
unLikePostCall(holder, api, credential)
|
||||
} else {
|
||||
// Button is inactive, like
|
||||
likePostCall(holder, api, credential)
|
||||
}
|
||||
//show animation or not?
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun likePostCall(
|
||||
holder : StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
) {
|
||||
//Call the api function
|
||||
status?.id?.let {
|
||||
api.likePost(credential, it).enqueue(object : Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("LIKE ERROR", t.toString())
|
||||
holder.liker.isChecked = false
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
|
||||
//Update shown like count and internal like toggle
|
||||
holder.nlikes.text = resp.getNLikes(holder.view.context)
|
||||
holder.liker.isChecked = resp.favourited ?: false
|
||||
} else {
|
||||
Log.e("RESPONSE_CODE", response.code().toString())
|
||||
holder.liker.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun unLikePostCall(
|
||||
holder : StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
) {
|
||||
//Call the api function
|
||||
status?.id?.let {
|
||||
api.unlikePost(credential, it).enqueue(object : Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("UNLIKE ERROR", t.toString())
|
||||
holder.liker.isChecked = true
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
|
||||
//Update shown like count and internal like toggle
|
||||
holder.nlikes.text = resp.getNLikes(holder.view.context)
|
||||
holder.liker.isChecked = resp.favourited ?: false
|
||||
} else {
|
||||
Log.e("RESPONSE_CODE", response.code().toString())
|
||||
holder.liker.isChecked = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun showComments(
|
||||
holder: StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String
|
||||
) {
|
||||
//Show all comments of a post
|
||||
if (status?.replies_count == 0) {
|
||||
holder.viewComment.text = holder.view.context.getString(R.string.NoCommentsToShow)
|
||||
} else {
|
||||
holder.viewComment.apply {
|
||||
text = "${status?.replies_count} ${holder.view.context.getString(R.string.CommentDisplay)}"
|
||||
setOnClickListener {
|
||||
visibility = View.GONE
|
||||
|
||||
//Retrieve the comments
|
||||
retrieveComments(holder, api, credential)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun activateCommenter(
|
||||
holder: StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String
|
||||
) {
|
||||
//Toggle comment button
|
||||
toggleCommentInput(holder)
|
||||
|
||||
//Activate commenterpostPicture
|
||||
holder.submitCmnt.setOnClickListener {
|
||||
val textIn = holder.comment.text
|
||||
//Open text input
|
||||
if(textIn.isNullOrEmpty()) {
|
||||
Toast.makeText(
|
||||
holder.view.context,
|
||||
holder.view.context.getString(R.string.empty_comment),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
|
||||
//Post the comment
|
||||
postComment(holder, api, credential)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleCommentInput(
|
||||
holder : StatusViewHolder
|
||||
) {
|
||||
//Toggle comment button
|
||||
holder.commenter.setOnClickListener {
|
||||
when(holder.commentIn.visibility) {
|
||||
View.VISIBLE -> {
|
||||
holder.commentIn.visibility = View.GONE
|
||||
ImageConverter.setImageFromDrawable(
|
||||
holder.view,
|
||||
holder.commenter,
|
||||
R.drawable.ic_comment_empty
|
||||
)
|
||||
}
|
||||
View.GONE -> {
|
||||
holder.commentIn.visibility = View.VISIBLE
|
||||
ImageConverter.setImageFromDrawable(
|
||||
holder.view,
|
||||
holder.commenter,
|
||||
R.drawable.ic_comment_blue
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addComment(context: android.content.Context, commentContainer: LinearLayout, commentUsername: String, commentContent: String) {
|
||||
|
||||
val view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.comment, commentContainer, true)
|
||||
|
||||
view.user.text = commentUsername
|
||||
view.commentText.text = commentContent
|
||||
}
|
||||
|
||||
fun retrieveComments(
|
||||
holder : StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
) {
|
||||
status?.id?.let {
|
||||
api.statusComments(it, credential).enqueue(object :
|
||||
Callback<Context> {
|
||||
override fun onFailure(call: Call<Context>, t: Throwable) {
|
||||
Log.e("COMMENT FETCH ERROR", t.toString())
|
||||
}
|
||||
|
||||
override fun onResponse(
|
||||
call: Call<Context>,
|
||||
response: Response<Context>
|
||||
) {
|
||||
if(response.code() == 200) {
|
||||
val statuses = response.body()!!.descendants
|
||||
|
||||
holder.commentCont.removeAllViews()
|
||||
|
||||
//Create the new views for each comment
|
||||
for (status in statuses) {
|
||||
addComment(holder.view.context, holder.commentCont, status.account!!.username!!,
|
||||
status.content!!
|
||||
)
|
||||
}
|
||||
holder.commentCont.visibility = View.VISIBLE
|
||||
} else {
|
||||
Log.e("COMMENT ERROR", "${response.code()} with body ${response.errorBody()}")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun postComment(
|
||||
holder : StatusViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
) {
|
||||
val textIn = holder.comment.text
|
||||
val nonNullText = textIn.toString()
|
||||
status?.id?.let {
|
||||
api.postStatus(credential, nonNullText, it).enqueue(object :
|
||||
Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("COMMENT ERROR", t.toString())
|
||||
Toast.makeText(
|
||||
holder.view.context, holder.view.context.getString(R.string.comment_error),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
//Check that the received response code is valid
|
||||
if (response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
holder.commentIn.visibility = View.GONE
|
||||
|
||||
//Add the comment to the comment section
|
||||
addComment(
|
||||
holder.view.context, holder.commentCont, resp.account!!.username!!,
|
||||
resp.content!!
|
||||
)
|
||||
|
||||
Toast.makeText(
|
||||
holder.view.context,
|
||||
holder.view.context.getString(R.string.comment_posted).format(textIn),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
Log.e("COMMENT SUCCESS", "posted: $textIn")
|
||||
} else {
|
||||
Log.e("ERROR_CODE", response.code().toString())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup): StatusViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.post_fragment, parent, false)
|
||||
return StatusViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AlbumViewPagerAdapter(private val media_attachments: List<Attachment>) : RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.album_image_view, parent, false))
|
||||
|
||||
override fun getItemCount() = media_attachments.size
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
Glide.with(holder.view)
|
||||
.asDrawable().fitCenter().placeholder(ColorDrawable(Color.GRAY))
|
||||
.load(media_attachments[position].url).into(holder.image)
|
||||
|
||||
val description = media_attachments[position].description
|
||||
.orEmpty().ifEmpty{ holder.view.context.getString(R.string.no_description)}
|
||||
|
||||
holder.image.setOnLongClickListener {
|
||||
Snackbar.make(it, description, Snackbar.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
|
||||
holder.image.contentDescription = description
|
||||
}
|
||||
|
||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view){
|
||||
val image: ImageView = view.findViewById(R.id.imageImageView)
|
||||
}
|
||||
}
|
@ -1,209 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_ID_TAG
|
||||
import com.h.pixeldroid.objects.Account.Companion.FOLLOWERS_TAG
|
||||
import kotlinx.android.synthetic.main.account_list_entry.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
open class AccountListFragment : FeedFragment() {
|
||||
lateinit var profilePicRequest: RequestBuilder<Drawable>
|
||||
protected lateinit var adapter : FeedsRecyclerViewAdapter<Account, AccountsRecyclerViewAdapter.ViewHolder>
|
||||
lateinit var factory: FeedDataSourceFactory<String, Account>
|
||||
lateinit var content: LiveData<PagedList<Account>>
|
||||
|
||||
private var currentPage = 1
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
//RequestBuilder that is re-used for every image
|
||||
profilePicRequest = Glide.with(this)
|
||||
.asDrawable().dontAnimate().apply(RequestOptions().circleCrop())
|
||||
.placeholder(R.drawable.ic_default_user)
|
||||
|
||||
adapter = AccountsRecyclerViewAdapter()
|
||||
list.adapter = adapter
|
||||
|
||||
//Make Glide be aware of the recyclerview and pre-load images
|
||||
val sizeProvider: ListPreloader.PreloadSizeProvider<Account> = ViewPreloadSizeProvider()
|
||||
val preloader: RecyclerViewPreloader<Account> = RecyclerViewPreloader(
|
||||
Glide.with(this), adapter as AccountListFragment.AccountsRecyclerViewAdapter, sizeProvider, 4
|
||||
)
|
||||
list.addOnScrollListener(preloader)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
content = makeContent()
|
||||
|
||||
content.observe(viewLifecycleOwner,
|
||||
Observer { c ->
|
||||
adapter.submitList(c)
|
||||
//after a refresh is done we need to stop the pull to refresh spinner
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
})
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
showError(show = false)
|
||||
|
||||
currentPage = 1
|
||||
//by invalidating data, loadInitial will be called again
|
||||
factory.liveData.value!!.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
internal open fun makeContent(): LiveData<PagedList<Account>> {
|
||||
val id = arguments?.getSerializable(ACCOUNT_ID_TAG) as String
|
||||
val following = arguments?.getSerializable(FOLLOWERS_TAG) as Boolean
|
||||
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
val dataSource = AccountListDataSource(following, id)
|
||||
factory = FeedDataSourceFactory(dataSource)
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
|
||||
inner class AccountListDataSource(private val following: Boolean, private val id: String) :
|
||||
FeedDataSource<String, Account>() {
|
||||
|
||||
override fun newSource(): AccountListDataSource {
|
||||
return AccountListDataSource(following, id)
|
||||
}
|
||||
|
||||
//We use the id as the key
|
||||
override fun getKey(item: Account): String {
|
||||
return currentPage.toString()
|
||||
}
|
||||
|
||||
override fun makeInitialCall(requestedLoadSize: Int): Call<List<Account>> {
|
||||
return if (following) {
|
||||
pixelfedAPI.followers(
|
||||
id, "Bearer $accessToken",
|
||||
limit = requestedLoadSize
|
||||
)
|
||||
} else {
|
||||
pixelfedAPI.following(
|
||||
id, "Bearer $accessToken",
|
||||
limit = requestedLoadSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Account>> {
|
||||
// Pixelfed and Mastodon don't implement this in the same fashion. Pixelfed uses
|
||||
// Laravel's paging mechanism, while Mastodon uses the Link header for pagination.
|
||||
// No need to know which is which, they should ignore the non-relevant argument
|
||||
return if (following) {
|
||||
pixelfedAPI.followers(
|
||||
id, "Bearer $accessToken",
|
||||
limit = requestedLoadSize, page = key, max_id = key
|
||||
)
|
||||
} else {
|
||||
pixelfedAPI.following(
|
||||
id, "Bearer $accessToken",
|
||||
limit = requestedLoadSize, page = key, max_id = key
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun enqueueCall(call: Call<List<Account>>, callback: LoadCallback<Account>){
|
||||
|
||||
call.enqueue(object : Callback<List<Account>> {
|
||||
override fun onResponse(call: Call<List<Account>>, response: Response<List<Account>>) {
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val data = response.body()!!
|
||||
if(response.headers()["Link"] != null){
|
||||
//Header is of the form:
|
||||
// Link: <https://mastodon.social/api/v1/accounts/1/followers?limit=2&max_id=7628164>; rel="next", <https://mastodon.social/api/v1/accounts/1/followers?limit=2&since_id=7628165>; 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 '?'
|
||||
currentPage = response.headers()["Link"]
|
||||
.orEmpty()
|
||||
.substringAfter("max_id=")
|
||||
.substringBefore('?')
|
||||
.substringBefore('>')
|
||||
.toIntOrNull() ?: 0
|
||||
} else {
|
||||
currentPage++
|
||||
}
|
||||
callback.onResult(data)
|
||||
} else{
|
||||
showError()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
loadingIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Account>>, t: Throwable) {
|
||||
showError(errorText = R.string.feed_failed)
|
||||
Log.e("AccountListFragment", t.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
inner class AccountsRecyclerViewAdapter : FeedsRecyclerViewAdapter<Account, AccountsRecyclerViewAdapter.ViewHolder>(),
|
||||
ListPreloader.PreloadModelProvider<Account> {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.account_list_entry, parent, false)
|
||||
context = view.context
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder : ViewHolder, position : Int) {
|
||||
val account = getItem(position) ?: return
|
||||
profilePicRequest.load(account.avatar).into(holder.avatar)
|
||||
|
||||
holder.username.text = account.username
|
||||
@SuppressLint("SetTextI18n")
|
||||
holder.acct.text = "@${account.acct}"
|
||||
|
||||
holder.mView.setOnClickListener { account.openProfile(context) }
|
||||
}
|
||||
|
||||
inner class ViewHolder(val mView : View) : RecyclerView.ViewHolder(mView) {
|
||||
val avatar : ImageView = mView.account_entry_avatar
|
||||
val username : TextView = mView.account_entry_username
|
||||
val acct: TextView = mView.account_entry_acct
|
||||
}
|
||||
|
||||
override fun getPreloadItems(position : Int) : MutableList<Account> {
|
||||
val account = getItem(position) ?: return mutableListOf()
|
||||
return mutableListOf(account)
|
||||
}
|
||||
|
||||
override fun getPreloadRequestBuilder(item : Account) : RequestBuilder<*>? {
|
||||
return profilePicRequest.load(item.avatar_static)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.size
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.LoadStateAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.databinding.FragmentFeedBinding
|
||||
import com.h.pixeldroid.databinding.LoadStateFooterViewItemBinding
|
||||
|
||||
/**
|
||||
* Shows or hides the error in the different FeedFragments
|
||||
*/
|
||||
private fun showError(errorText: String, show: Boolean = true, binding: FragmentFeedBinding){
|
||||
if(show){
|
||||
binding.motionLayout.transitionToEnd()
|
||||
binding.errorLayout.errorText.text = errorText
|
||||
} else if(binding.motionLayout.progress == 1F){
|
||||
binding.motionLayout.transitionToStart()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the [RecyclerView] adapter for the different FeedFragments.
|
||||
*
|
||||
* Makes the UI respond to various [LoadState]s, including errors when an error message is shown.
|
||||
*/
|
||||
internal fun <T: Any> initAdapter(binding: FragmentFeedBinding, adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>) {
|
||||
binding.list.adapter = adapter.withLoadStateFooter(
|
||||
footer = ReposLoadStateAdapter { adapter.retry() }
|
||||
)
|
||||
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
|
||||
if(!binding.progressBar.isVisible && binding.swipeRefreshLayout.isRefreshing) {
|
||||
// Stop loading spinner when loading is done
|
||||
binding.swipeRefreshLayout.isRefreshing = loadState.refresh is LoadState.Loading
|
||||
} else {
|
||||
// ProgressBar should stop showing as soon as the source stops loading ("source"
|
||||
// meaning the database, so don't wait on the network)
|
||||
val sourceLoading = loadState.source.refresh is LoadState.Loading
|
||||
if(!sourceLoading && binding.list.size > 0){
|
||||
binding.list.isVisible = true
|
||||
binding.progressBar.isVisible = false
|
||||
} else if(binding.list.size == 0
|
||||
&& loadState.append is LoadState.NotLoading
|
||||
&& loadState.append.endOfPaginationReached){
|
||||
binding.progressBar.isVisible = false
|
||||
showError(binding = binding, errorText = "Nothing to see here :(")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Toast on any error, regardless of whether it came from RemoteMediator or PagingSource
|
||||
val errorState = loadState.source.append as? LoadState.Error
|
||||
?: loadState.source.prepend as? LoadState.Error
|
||||
?: loadState.source.refresh as? LoadState.Error
|
||||
?: loadState.append as? LoadState.Error
|
||||
?: loadState.prepend as? LoadState.Error
|
||||
?: loadState.refresh as? LoadState.Error
|
||||
errorState?.let {
|
||||
showError(binding = binding, errorText = it.error.toString())
|
||||
}
|
||||
if (errorState == null) showError(binding = binding, show = false, errorText = "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter to the show the a [RecyclerView] item for a [LoadState], with a callback to retry if
|
||||
* the retry button is pressed.
|
||||
*/
|
||||
class ReposLoadStateAdapter(
|
||||
private val retry: () -> Unit
|
||||
) : LoadStateAdapter<ReposLoadStateViewHolder>() {
|
||||
override fun onBindViewHolder(holder: ReposLoadStateViewHolder, loadState: LoadState) {
|
||||
holder.bind(loadState)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ReposLoadStateViewHolder {
|
||||
return ReposLoadStateViewHolder.create(parent, retry)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [RecyclerView.ViewHolder] that is shown at the end of the feed to indicate loading or errors
|
||||
* in the loading of appending values.
|
||||
*/
|
||||
class ReposLoadStateViewHolder(
|
||||
private val binding: LoadStateFooterViewItemBinding,
|
||||
retry: () -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
init {
|
||||
binding.retryButton.setOnClickListener { retry.invoke() }
|
||||
}
|
||||
|
||||
fun bind(loadState: LoadState) {
|
||||
if (loadState is LoadState.Error) {
|
||||
binding.errorMsg.text = loadState.error.localizedMessage
|
||||
}
|
||||
binding.progressBar.isVisible = loadState is LoadState.Loading
|
||||
binding.retryButton.isVisible = loadState !is LoadState.Loading
|
||||
binding.errorMsg.isVisible = loadState !is LoadState.Loading
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup, retry: () -> Unit): ReposLoadStateViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.load_state_footer_view_item, parent, false)
|
||||
val binding = LoadStateFooterViewItemBinding.bind(view)
|
||||
return ReposLoadStateViewHolder(binding, retry)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,165 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import androidx.paging.ItemKeyedDataSource
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.h.pixeldroid.Pixeldroid
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.objects.FeedContent
|
||||
import kotlinx.android.synthetic.main.fragment_feed.*
|
||||
import kotlinx.android.synthetic.main.fragment_feed.view.*
|
||||
import org.w3c.dom.Text
|
||||
import retrofit2.Call
|
||||
import javax.inject.Inject
|
||||
|
||||
open class FeedFragment: Fragment() {
|
||||
|
||||
protected var accessToken: String? = null
|
||||
@Inject
|
||||
lateinit var apiHolder: PixelfedAPIHolder
|
||||
|
||||
protected lateinit var pixelfedAPI: PixelfedAPI
|
||||
|
||||
protected lateinit var list : RecyclerView
|
||||
protected lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||
internal lateinit var loadingIndicator: ProgressBar
|
||||
var user: UserDatabaseEntity? = null
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_feed, container, false)
|
||||
|
||||
(requireActivity().application as Pixeldroid).getAppComponent().inject(this)
|
||||
|
||||
//Initialize lateinit fields that are needed as soon as the view is created
|
||||
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
|
||||
loadingIndicator = view.findViewById(R.id.progressBar)
|
||||
list = swipeRefreshLayout.list
|
||||
list.layoutManager = LinearLayoutManager(context)
|
||||
user = db.userDao().getActiveUser()
|
||||
|
||||
pixelfedAPI = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
|
||||
accessToken = user?.accessToken.orEmpty()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
fun showError(@StringRes errorText: Int = R.string.loading_toast, show: Boolean = true){
|
||||
val errorLayout = view?.findViewById<ConstraintLayout>(R.id.errorLayout)
|
||||
val progressBar = view?.findViewById<ProgressBar>(R.id.progressBar)
|
||||
|
||||
if(show){
|
||||
view?.findViewById<TextView>(R.id.error_text)?.setText(errorText)
|
||||
errorLayout?.visibility = VISIBLE
|
||||
progressBar?.visibility = GONE
|
||||
} else {
|
||||
errorLayout?.visibility = GONE
|
||||
progressBar?.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
open inner class FeedDataSourceFactory<ObjectId, APIObject: FeedContent>(
|
||||
private val dataSource: FeedDataSource<ObjectId, APIObject>
|
||||
): DataSource.Factory<ObjectId, APIObject>() {
|
||||
internal lateinit var liveData: MutableLiveData<FeedDataSource<ObjectId, APIObject>>
|
||||
|
||||
override fun create(): DataSource<ObjectId, APIObject> {
|
||||
val dataSource = dataSource.newSource()
|
||||
liveData = MutableLiveData()
|
||||
liveData.postValue(dataSource)
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
abstract inner class FeedDataSource<ObjectId, APIObject: FeedContent>: ItemKeyedDataSource<ObjectId, APIObject>(){
|
||||
|
||||
/**
|
||||
* Used in the initial call to initialize the list [loadInitial].
|
||||
* @param requestedLoadSize number of objects requested in a call
|
||||
* @return [Call] that gets the list of [APIObject]
|
||||
*/
|
||||
abstract fun makeInitialCall(requestedLoadSize: Int): Call<List<APIObject>>
|
||||
|
||||
/**
|
||||
* Used in the subsequent calls to get more objects.
|
||||
* @param requestedLoadSize number of objects requested in a call
|
||||
* @param key of the last object we already have
|
||||
* @return [Call] that gets the list of [APIObject]
|
||||
*/
|
||||
abstract fun makeAfterCall(requestedLoadSize: Int, key: ObjectId): Call<List<APIObject>>
|
||||
|
||||
/**
|
||||
* This is called to initialize the list, so we want some of the most recent objects.
|
||||
* @param params holds the requestedLoadSize
|
||||
* @param callback to call after network request completes
|
||||
*/
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<ObjectId>,
|
||||
callback: LoadInitialCallback<APIObject>
|
||||
) {
|
||||
enqueueCall(makeInitialCall(params.requestedLoadSize), callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called to when we get to the bottom of the loaded content, so we want objects
|
||||
* older than the given key (params.key).
|
||||
* @param params holds the requestedLoadSize
|
||||
* @param callback to call after network request completes
|
||||
*/
|
||||
override fun loadAfter(params: LoadParams<ObjectId>, callback: LoadCallback<APIObject>) {
|
||||
enqueueCall(makeAfterCall(params.requestedLoadSize, params.key), callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing here, it is expected to pull to refresh to load newer items
|
||||
*/
|
||||
override fun loadBefore(params: LoadParams<ObjectId>, callback: LoadCallback<APIObject>) {}
|
||||
|
||||
abstract fun enqueueCall(call: Call<List<APIObject>>, callback: LoadCallback<APIObject>)
|
||||
|
||||
abstract fun newSource(): FeedDataSource<ObjectId, APIObject>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FeedsRecyclerViewAdapter<T: FeedContent, VH : RecyclerView.ViewHolder?>: PagedListAdapter<T, VH>(
|
||||
object : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem.id === newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem.equals(newItem)
|
||||
}
|
||||
}
|
||||
){
|
||||
protected lateinit var context: Context
|
||||
}
|
||||
|
@ -1,269 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider
|
||||
import com.h.pixeldroid.PostActivity
|
||||
import com.h.pixeldroid.ProfileActivity
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Notification
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.HtmlUtils.Companion.parseHTMLText
|
||||
import com.h.pixeldroid.utils.Utils.Companion.setTextViewFromISO8601
|
||||
import kotlinx.android.synthetic.main.fragment_notifications.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a list of Items.
|
||||
*/
|
||||
class NotificationsFragment : FeedFragment() {
|
||||
|
||||
lateinit var profilePicRequest: RequestBuilder<Drawable>
|
||||
protected lateinit var adapter : FeedsRecyclerViewAdapter<Notification, NotificationsRecyclerViewAdapter.ViewHolder>
|
||||
lateinit var factory: FeedDataSourceFactory<String, Notification>
|
||||
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
//RequestBuilder that is re-used for every image
|
||||
profilePicRequest = Glide.with(this)
|
||||
.asDrawable().apply(RequestOptions().circleCrop())
|
||||
.placeholder(R.drawable.ic_default_user)
|
||||
|
||||
|
||||
adapter = NotificationsRecyclerViewAdapter()
|
||||
list.adapter = adapter
|
||||
|
||||
|
||||
//Make Glide be aware of the recyclerview and pre-load images
|
||||
val sizeProvider: ListPreloader.PreloadSizeProvider<Notification> = ViewPreloadSizeProvider()
|
||||
val preloader: RecyclerViewPreloader<Notification> = RecyclerViewPreloader(
|
||||
Glide.with(this), adapter as NotificationsFragment.NotificationsRecyclerViewAdapter, sizeProvider, 4
|
||||
)
|
||||
list.addOnScrollListener(preloader)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val content = makeContent()
|
||||
|
||||
content.observe(viewLifecycleOwner,
|
||||
Observer { c ->
|
||||
adapter.submitList(c)
|
||||
//after a refresh is done we need to stop the pull to refresh spinner
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
})
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
showError(show = false)
|
||||
|
||||
//by invalidating data, loadInitial will be called again
|
||||
factory.liveData.value!!.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeContent(): LiveData<PagedList<Notification>> {
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
val dataSource = NotificationListDataSource()
|
||||
factory = FeedDataSourceFactory(dataSource)
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
|
||||
inner class NotificationListDataSource: FeedDataSource<String, Notification>() {
|
||||
|
||||
override fun newSource(): NotificationListDataSource {
|
||||
return NotificationListDataSource()
|
||||
}
|
||||
|
||||
//We use the id as the key
|
||||
override fun getKey(item: Notification): String {
|
||||
return item.id
|
||||
}
|
||||
|
||||
override fun makeInitialCall(requestedLoadSize: Int): Call<List<Notification>> {
|
||||
return pixelfedAPI
|
||||
.notifications("Bearer $accessToken", limit="$requestedLoadSize")
|
||||
}
|
||||
override fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Notification>> {
|
||||
return pixelfedAPI
|
||||
.notifications("Bearer $accessToken", max_id=key, limit="$requestedLoadSize")
|
||||
}
|
||||
|
||||
override fun enqueueCall(call: Call<List<Notification>>, callback: LoadCallback<Notification>){
|
||||
|
||||
call.enqueue(object : Callback<List<Notification>> {
|
||||
override fun onResponse(call: Call<List<Notification>>, response: Response<List<Notification>>) {
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val data = response.body()!!
|
||||
callback.onResult(data)
|
||||
} else {
|
||||
showError()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
loadingIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Notification>>, t: Throwable) {
|
||||
showError(errorText = R.string.feed_failed)
|
||||
Log.e("NotificationsFragment", t.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [RecyclerView.Adapter] that can display a [Notification]
|
||||
*/
|
||||
inner class NotificationsRecyclerViewAdapter: FeedsRecyclerViewAdapter<Notification, NotificationsRecyclerViewAdapter.ViewHolder>(),
|
||||
ListPreloader.PreloadModelProvider<Notification> {
|
||||
|
||||
private val mOnClickListener: View.OnClickListener
|
||||
|
||||
init {
|
||||
mOnClickListener = View.OnClickListener { v ->
|
||||
val notification = v.tag as Notification
|
||||
openActivity(notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPostFromNotification(notification: Notification) : Intent {
|
||||
val intent = Intent(context, PostActivity::class.java)
|
||||
intent.putExtra(Status.POST_TAG, notification.status)
|
||||
return intent
|
||||
}
|
||||
|
||||
private fun openActivity(notification: Notification){
|
||||
val intent: Intent
|
||||
when (notification.type){
|
||||
Notification.NotificationType.mention, Notification.NotificationType.favourite,
|
||||
Notification.NotificationType.poll, Notification.NotificationType.reblog -> {
|
||||
intent = openPostFromNotification(notification)
|
||||
}
|
||||
Notification.NotificationType.follow -> {
|
||||
intent = Intent(context, ProfileActivity::class.java)
|
||||
intent.putExtra(Account.ACCOUNT_TAG, notification.account)
|
||||
}
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.fragment_notifications, parent, false)
|
||||
context = view.context
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val notification = getItem(position) ?: return
|
||||
profilePicRequest.load(notification.account.avatar_static).into(holder.avatar)
|
||||
|
||||
val previewUrl = notification.status?.media_attachments?.getOrNull(0)?.preview_url
|
||||
if(!previewUrl.isNullOrBlank()){
|
||||
Glide.with(holder.mView).load(previewUrl)
|
||||
.placeholder(R.drawable.ic_picture_fallback).into(holder.photoThumbnail)
|
||||
} else{
|
||||
holder.photoThumbnail.visibility = View.GONE
|
||||
}
|
||||
|
||||
setNotificationType(notification.type, notification.account.username!!, holder.notificationType)
|
||||
setTextViewFromISO8601(notification.created_at, holder.notificationTime, false, context)
|
||||
|
||||
//Convert HTML to clickable text
|
||||
holder.postDescription.text =
|
||||
parseHTMLText(
|
||||
notification.status?.content ?: "",
|
||||
notification.status?.mentions,
|
||||
pixelfedAPI,
|
||||
context,
|
||||
"Bearer $accessToken"
|
||||
)
|
||||
|
||||
|
||||
with(holder.mView) {
|
||||
tag = notification
|
||||
setOnClickListener(mOnClickListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNotificationType(type: Notification.NotificationType, username: String,
|
||||
textView: TextView
|
||||
){
|
||||
val context = textView.context
|
||||
val (format: String, drawable: Drawable?) = when(type) {
|
||||
Notification.NotificationType.follow -> {
|
||||
setNotificationTypeTextView(context, R.string.followed_notification, R.drawable.ic_follow)
|
||||
}
|
||||
Notification.NotificationType.mention -> {
|
||||
setNotificationTypeTextView(context, R.string.mention_notification, R.drawable.ic_apenstaart)
|
||||
}
|
||||
|
||||
Notification.NotificationType.reblog -> {
|
||||
setNotificationTypeTextView(context, R.string.shared_notification, R.drawable.ic_reblog_blue)
|
||||
}
|
||||
|
||||
Notification.NotificationType.favourite -> {
|
||||
setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_like_full)
|
||||
}
|
||||
Notification.NotificationType.poll -> {
|
||||
setNotificationTypeTextView(context, R.string.poll_notification, R.drawable.poll)
|
||||
}
|
||||
}
|
||||
textView.text = format.format(username)
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
drawable,null,null,null
|
||||
)
|
||||
}
|
||||
private fun setNotificationTypeTextView(context: Context, format: Int, drawable: Int): Pair<String, Drawable?> {
|
||||
return Pair(context.getString(format), context.getDrawable(drawable))
|
||||
}
|
||||
|
||||
|
||||
inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) {
|
||||
val notificationType: TextView = mView.notification_type
|
||||
val notificationTime: TextView = mView.notification_time
|
||||
val postDescription: TextView = mView.notification_post_description
|
||||
val avatar: ImageView = mView.notification_avatar
|
||||
val photoThumbnail: ImageView = mView.notification_photo_thumbnail
|
||||
}
|
||||
|
||||
override fun getPreloadItems(position: Int): MutableList<Notification> {
|
||||
val notification = getItem(position) ?: return mutableListOf()
|
||||
return mutableListOf(notification)
|
||||
}
|
||||
|
||||
override fun getPreloadRequestBuilder(item: Notification): RequestBuilder<*>? {
|
||||
return profilePicRequest.load(item.account.avatar_static)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,211 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.h.pixeldroid.MainActivity
|
||||
import com.h.pixeldroid.Pixeldroid
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.PostDatabaseEntity
|
||||
import com.h.pixeldroid.fragments.ImageFragment
|
||||
import com.h.pixeldroid.utils.*
|
||||
import com.h.pixeldroid.utils.Utils.Companion.setTextViewFromISO8601
|
||||
import kotlinx.android.synthetic.main.fragment_offline_feed.view.*
|
||||
import kotlinx.android.synthetic.main.post_fragment.view.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class OfflineFeedFragment: Fragment() {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var viewAdapter: RecyclerView.Adapter<*>
|
||||
private lateinit var viewManager: RecyclerView.LayoutManager
|
||||
lateinit var picRequest: RequestBuilder<Drawable>
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// Inflate the layout for this fragment
|
||||
val view = inflater.inflate(R.layout.fragment_offline_feed, container, false)
|
||||
val loadingAnimation = view.offline_feed_progress_bar
|
||||
loadingAnimation.visibility = View.VISIBLE
|
||||
picRequest = Glide.with(this)
|
||||
.asDrawable().fitCenter()
|
||||
.placeholder(ColorDrawable(Color.GRAY))
|
||||
|
||||
(requireActivity().application as Pixeldroid).getAppComponent().inject(this)
|
||||
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
if (db.postDao().numberOfPosts(user.user_id, user.instance_uri) > 0) {
|
||||
val posts = db.postDao().getAll(user.user_id, user.instance_uri)
|
||||
viewManager = LinearLayoutManager(requireContext())
|
||||
viewAdapter = OfflinePostFeedAdapter(posts)
|
||||
loadingAnimation.visibility = View.GONE
|
||||
recyclerView = view.offline_feed_recyclerview.apply {
|
||||
visibility = View.VISIBLE
|
||||
// use this setting to improve performance if you know that changes
|
||||
// in content do not change the layout size of the RecyclerView
|
||||
setHasFixedSize(true)
|
||||
// use a linear layout manager
|
||||
layoutManager = viewManager
|
||||
// specify an viewAdapter (see also next example)
|
||||
adapter = viewAdapter
|
||||
}
|
||||
} else {
|
||||
loadingAnimation.visibility = View.GONE
|
||||
view.offline_feed_placeholder_text.visibility = View.VISIBLE
|
||||
}
|
||||
view.offline_feed_progress_bar.visibility = View.GONE
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
view.swipeRefreshLayout.setOnRefreshListener {
|
||||
if (Utils.hasInternet(requireContext())) {
|
||||
onStop()
|
||||
startActivity(Intent(requireContext(), MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
})
|
||||
}
|
||||
view.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
inner class OfflinePostFeedAdapter(private val posts: List<PostDatabaseEntity>)
|
||||
: RecyclerView.Adapter<OfflinePostFeedAdapter.OfflinePostViewHolder>() {
|
||||
|
||||
inner class OfflinePostViewHolder(private val postView: View)
|
||||
: RecyclerView.ViewHolder(postView) {
|
||||
val profilePic : ImageView = postView.findViewById(R.id.profilePic)
|
||||
val postPic : ImageView = postView.findViewById(R.id.postPicture)
|
||||
val username : TextView = postView.findViewById(R.id.username)
|
||||
val description : TextView = postView.findViewById(R.id.description)
|
||||
val comment : EditText = postView.findViewById(R.id.editComment)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OfflinePostViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.post_fragment, parent, false)
|
||||
.apply {
|
||||
commenter.visibility = View.GONE
|
||||
postDomain.visibility = View.GONE
|
||||
commentIn.visibility = View.GONE
|
||||
liker.apply {
|
||||
//de-activate the liker
|
||||
setEventListener { _, _ ->
|
||||
false
|
||||
}
|
||||
}
|
||||
reblogger.apply {
|
||||
//de-activate the reblogger
|
||||
setEventListener { _, _ ->
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
return OfflinePostViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: OfflinePostViewHolder, position: Int) {
|
||||
val post = posts[position]
|
||||
val metrics = requireContext().resources.displayMetrics
|
||||
//Limit the height of the different images
|
||||
holder.profilePic.maxHeight = metrics.heightPixels
|
||||
holder.postPic.maxHeight = metrics.heightPixels
|
||||
//Setup username as a button that opens the profile
|
||||
holder.itemView.username.apply {
|
||||
text = post.account_name
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
//Convert the date to a readable string
|
||||
setTextViewFromISO8601(post.date, holder.itemView.postDate, false, requireContext())
|
||||
|
||||
//Setup images
|
||||
ImageConverter.setRoundImageFromURL(
|
||||
holder.itemView,
|
||||
post.account_profile_picture,
|
||||
holder.profilePic
|
||||
)
|
||||
|
||||
//Setup post pic only if there are media attachments
|
||||
if(!post.media_urls.isNullOrEmpty()) {
|
||||
// Standard layout
|
||||
holder.postPic.visibility = View.VISIBLE
|
||||
holder.itemView.postPager.visibility = View.GONE
|
||||
holder.itemView.postTabs.visibility = View.GONE
|
||||
holder.itemView.sensitiveWarning.visibility = View.GONE
|
||||
|
||||
if(post.media_urls.size == 1) {
|
||||
picRequest.load(post.media_urls[0]).into(holder.postPic)
|
||||
} else {
|
||||
//Only show the viewPager and tabs
|
||||
holder.postPic.visibility = View.GONE
|
||||
holder.itemView.postPager.visibility = View.VISIBLE
|
||||
holder.itemView.postTabs.visibility = View.VISIBLE
|
||||
val tabs : ArrayList<ImageFragment> = ArrayList()
|
||||
//Fill the tabs with each mediaAttachment
|
||||
for((index, media) in post.media_urls.withIndex()) {
|
||||
tabs.add(ImageFragment.newInstance(media, "Photo $index"))
|
||||
}
|
||||
holder.itemView.postPager.adapter = object : FragmentStateAdapter(this@OfflineFeedFragment) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return tabs[position]
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return post.media_urls.size
|
||||
}
|
||||
}
|
||||
TabLayoutMediator(holder.itemView.postTabs, holder.itemView.postPager) { tab, _ ->
|
||||
tab.icon = holder.itemView.context.getDrawable(R.drawable.ic_dot_blue_12dp)
|
||||
}.attach()
|
||||
}
|
||||
}
|
||||
|
||||
holder.description.apply {
|
||||
if(post.description.isBlank()) {
|
||||
visibility = View.GONE
|
||||
} else {
|
||||
text = HtmlUtils.fromHtml(post.description)
|
||||
}
|
||||
}
|
||||
|
||||
holder.itemView.nlikes.text = post.likes.toString()
|
||||
holder.itemView.nshares.text = post.shares.toString()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return posts.size
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,107 @@
|
||||
package com.h.pixeldroid.fragments.feeds.cachedFeeds
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import com.h.pixeldroid.databinding.FragmentFeedBinding
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.dao.feedContent.FeedContentDao
|
||||
import com.h.pixeldroid.fragments.BaseFragment
|
||||
import com.h.pixeldroid.fragments.feeds.initAdapter
|
||||
import com.h.pixeldroid.objects.FeedContentDatabase
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a list of [FeedContentDatabase] items that are cached by the database.
|
||||
*/
|
||||
open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
|
||||
|
||||
internal lateinit var viewModel: FeedViewModel<T>
|
||||
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
|
||||
|
||||
private lateinit var binding: FragmentFeedBinding
|
||||
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
|
||||
internal fun launch() {
|
||||
// Make sure we cancel the previous job before creating a new one
|
||||
job?.cancel()
|
||||
job = lifecycleScope.launch {
|
||||
viewModel.flow().collectLatest {
|
||||
adapter.submitData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun initSearch() {
|
||||
// Scroll to top when the list is refreshed from network.
|
||||
lifecycleScope.launch {
|
||||
adapter.loadStateFlow
|
||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
.distinctUntilChangedBy { it.refresh }
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is LoadState.NotLoading }
|
||||
.collect { binding.list.scrollToPosition(0) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
binding = FragmentFeedBinding.inflate(layoutInflater)
|
||||
|
||||
initAdapter(binding, adapter)
|
||||
|
||||
//binding.progressBar.visibility = View.GONE
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
//It shouldn't be necessary to also retry() in addition to refresh(),
|
||||
//but if we don't do this, reloads after an error fail immediately...
|
||||
// https://issuetracker.google.com/issues/173438474
|
||||
adapter.retry()
|
||||
adapter.refresh()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Factory that creates ViewModel from a [FeedContentRepository], to be used in cached feeds to
|
||||
* fetch the ViewModel that is responsible for preparing and managing the data for
|
||||
* an Activity or a Fragment
|
||||
*/
|
||||
class ViewModelFactory<U: FeedContentDatabase> @ExperimentalPagingApi constructor(private val db: AppDatabase?,
|
||||
private val dao: FeedContentDao<U>?,
|
||||
private val remoteMediator: RemoteMediator<Int, U>?,
|
||||
private val feedContentRepository: FeedContentRepository<U> = FeedContentRepository(db!!, dao!!, remoteMediator!!)
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(FeedViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return FeedViewModel(feedContentRepository) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.fragments.feeds.cachedFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.dao.feedContent.FeedContentDao
|
||||
import com.h.pixeldroid.objects.FeedContentDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Repository class that works with local and remote data sources.
|
||||
*/
|
||||
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi
|
||||
@Inject constructor(
|
||||
private val db: AppDatabase,
|
||||
private val dao: FeedContentDao<T>,
|
||||
private val mediator: RemoteMediator<Int, T>
|
||||
) {
|
||||
|
||||
/**
|
||||
* [FeedContentDatabase], exposed as a stream of data that will emit
|
||||
* every time we get more data from the network.
|
||||
*/
|
||||
fun stream(): Flow<PagingData<T>> {
|
||||
|
||||
val pagingSourceFactory = {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
dao.feedContent(user.user_id, user.instance_uri)
|
||||
}
|
||||
|
||||
return Pager(
|
||||
config = PagingConfig(initialLoadSize = NETWORK_PAGE_SIZE,
|
||||
pageSize = NETWORK_PAGE_SIZE,
|
||||
enablePlaceholders = false,
|
||||
prefetchDistance = 50
|
||||
),
|
||||
remoteMediator = mediator,
|
||||
pagingSourceFactory = pagingSourceFactory
|
||||
).flow
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NETWORK_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.fragments.feeds.cachedFeeds
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.h.pixeldroid.objects.FeedContentDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* ViewModel for the cached feeds.
|
||||
* The ViewModel works with the [FeedContentRepository] to get the data.
|
||||
*/
|
||||
class FeedViewModel<T: FeedContentDatabase>(private val repository: FeedContentRepository<T>) : ViewModel() {
|
||||
|
||||
private var currentResult: Flow<PagingData<T>>? = null
|
||||
|
||||
fun flow(): Flow<PagingData<T>> {
|
||||
val lastResult = currentResult
|
||||
if (lastResult != null) {
|
||||
return lastResult
|
||||
}
|
||||
val newResult: Flow<PagingData<T>> = repository.stream()
|
||||
.cachedIn(viewModelScope)
|
||||
currentResult = newResult
|
||||
return newResult
|
||||
}
|
||||
}
|
@ -0,0 +1,208 @@
|
||||
package com.h.pixeldroid.fragments.feeds.cachedFeeds.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.h.pixeldroid.PostActivity
|
||||
import com.h.pixeldroid.ProfileActivity
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Notification
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.Utils.Companion.setTextViewFromISO8601
|
||||
import kotlinx.android.synthetic.main.fragment_notifications.view.*
|
||||
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.CachedFeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.FeedViewModel
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.ViewModelFactory
|
||||
import com.h.pixeldroid.utils.HtmlUtils.Companion.parseHTMLText
|
||||
|
||||
|
||||
/**
|
||||
* Fragment for the notifications tab.
|
||||
*/
|
||||
class NotificationsFragment : CachedFeedFragment<Notification>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
adapter = NotificationsAdapter(apiHolder, db)
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(this, ViewModelFactory(db, db.notificationDao(), NotificationsRemoteMediator(apiHolder, db)))
|
||||
.get(FeedViewModel::class.java) as FeedViewModel<Notification>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* View Holder for a [Notification] RecyclerView list item.
|
||||
*/
|
||||
class NotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val notificationType: TextView = view.notification_type
|
||||
private val notificationTime: TextView = view.notification_time
|
||||
private val postDescription: TextView = view.notification_post_description
|
||||
private val avatar: ImageView = view.notification_avatar
|
||||
private val photoThumbnail: ImageView = view.notification_photo_thumbnail
|
||||
|
||||
private var notification: Notification? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
notification?.openActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Notification.openActivity() {
|
||||
val intent: Intent
|
||||
when (type){
|
||||
Notification.NotificationType.mention, Notification.NotificationType.favourite,
|
||||
Notification.NotificationType.poll, Notification.NotificationType.reblog -> {
|
||||
intent = openPostFromNotification()
|
||||
}
|
||||
Notification.NotificationType.follow -> {
|
||||
intent = Intent(itemView.context, ProfileActivity::class.java)
|
||||
intent.putExtra(Account.ACCOUNT_TAG, account)
|
||||
}
|
||||
}
|
||||
itemView.context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun Notification.openPostFromNotification(): Intent {
|
||||
val intent = Intent(itemView.context, PostActivity::class.java)
|
||||
intent.putExtra(Status.POST_TAG, status)
|
||||
return intent
|
||||
}
|
||||
|
||||
|
||||
private fun setNotificationType(type: Notification.NotificationType, username: String,
|
||||
textView: TextView
|
||||
){
|
||||
val context = textView.context
|
||||
val (format: String, drawable: Drawable?) = when(type) {
|
||||
Notification.NotificationType.follow -> {
|
||||
setNotificationTypeTextView(context, R.string.followed_notification, R.drawable.ic_follow)
|
||||
}
|
||||
Notification.NotificationType.mention -> {
|
||||
setNotificationTypeTextView(context, R.string.mention_notification, R.drawable.ic_apenstaart)
|
||||
}
|
||||
|
||||
Notification.NotificationType.reblog -> {
|
||||
setNotificationTypeTextView(context, R.string.shared_notification, R.drawable.ic_reblog_blue)
|
||||
}
|
||||
|
||||
Notification.NotificationType.favourite -> {
|
||||
setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_like_full)
|
||||
}
|
||||
Notification.NotificationType.poll -> {
|
||||
setNotificationTypeTextView(context, R.string.poll_notification, R.drawable.poll)
|
||||
}
|
||||
}
|
||||
textView.text = format.format(username)
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
drawable,null,null,null
|
||||
)
|
||||
}
|
||||
private fun setNotificationTypeTextView(context: Context, format: Int, drawable: Int): Pair<String, Drawable?> {
|
||||
return Pair(context.getString(format), ContextCompat.getDrawable(context, drawable))
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun bind(notification: Notification?, api: PixelfedAPI, accessToken: String) {
|
||||
|
||||
this.notification = notification
|
||||
|
||||
Glide.with(itemView).load(notification?.account?.avatar_static).circleCrop().into(avatar)
|
||||
|
||||
val previewUrl = notification?.status?.media_attachments?.getOrNull(0)?.preview_url
|
||||
if(!previewUrl.isNullOrBlank()){
|
||||
Glide.with(itemView).load(previewUrl)
|
||||
.placeholder(R.drawable.ic_picture_fallback).into(photoThumbnail)
|
||||
} else{
|
||||
photoThumbnail.visibility = View.GONE
|
||||
}
|
||||
|
||||
notification?.type?.let { setNotificationType(it, notification.account.username!!, notificationType) }
|
||||
notification?.created_at?.let { setTextViewFromISO8601(it, notificationTime, false, itemView.context) }
|
||||
|
||||
//Convert HTML to clickable text
|
||||
postDescription.text =
|
||||
parseHTMLText(
|
||||
notification?.status?.content ?: "",
|
||||
notification?.status?.mentions,
|
||||
api,
|
||||
itemView.context,
|
||||
"Bearer $accessToken"
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup): NotificationViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.fragment_notifications, parent, false)
|
||||
return NotificationViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NotificationsAdapter(private val apiHolder: PixelfedAPIHolder, private val db: AppDatabase) : PagingDataAdapter<Notification, RecyclerView.ViewHolder>(
|
||||
UIMODEL_COMPARATOR
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return NotificationViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.fragment_notifications
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position)
|
||||
uiModel.let {
|
||||
(holder as NotificationViewHolder).bind(it, apiHolder.setDomainToCurrentUser(db), db.userDao().getActiveUser()!!.accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val UIMODEL_COMPARATOR = object : DiffUtil.ItemCallback<Notification>() {
|
||||
override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.fragments.feeds.cachedFeeds.notifications
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.room.withTransaction
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.objects.Notification
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* RemoteMediator for the notifications.
|
||||
*
|
||||
* A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote
|
||||
* source into a local source wrapped by a [PagingSource], e.g., loading data from network into
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class NotificationsRemoteMediator @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase
|
||||
) : RemoteMediator<Int, Notification>() {
|
||||
|
||||
override suspend fun load(loadType: LoadType, state: PagingState<Int, Notification>): MediatorResult {
|
||||
|
||||
val (max_id, min_id) = when (loadType) {
|
||||
LoadType.REFRESH -> {
|
||||
Pair<String?, String?>(null, null)
|
||||
}
|
||||
LoadType.PREPEND -> {
|
||||
//No prepend for the moment, might be nice to add later
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
LoadType.APPEND -> {
|
||||
Pair<String?, String?>(state.lastItemOrNull()?.id, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
|
||||
val accessToken = user.accessToken.orEmpty()
|
||||
|
||||
val apiResponse = api.notifications("Bearer $accessToken",
|
||||
max_id = max_id,
|
||||
min_id = min_id,
|
||||
limit = state.config.pageSize.toString(),
|
||||
)
|
||||
|
||||
apiResponse.forEach{it.user_id = user.user_id; it.instance_uri = user.instance_uri}
|
||||
|
||||
val endOfPaginationReached = apiResponse.isEmpty()
|
||||
|
||||
db.withTransaction {
|
||||
// clear table in the database
|
||||
if (loadType == LoadType.REFRESH) {
|
||||
db.notificationDao().clearFeedContent()
|
||||
}
|
||||
db.notificationDao().insertAll(apiResponse)
|
||||
}
|
||||
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
|
||||
} catch (exception: IOException) {
|
||||
return MediatorResult.Error(exception)
|
||||
} catch (exception: HttpException) {
|
||||
return MediatorResult.Error(exception)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package com.h.pixeldroid.fragments.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.room.withTransaction
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import com.h.pixeldroid.db.entities.HomeStatusDatabaseEntity
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* RemoteMediator for the home feed.
|
||||
*
|
||||
* A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote
|
||||
* source into a local source wrapped by a [PagingSource], e.g., loading data from network into
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class HomeFeedRemoteMediator @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase,
|
||||
) : RemoteMediator<Int, HomeStatusDatabaseEntity>() {
|
||||
|
||||
override suspend fun load(loadType: LoadType, state: PagingState<Int, HomeStatusDatabaseEntity>): MediatorResult {
|
||||
|
||||
val (max_id, min_id) = when (loadType) {
|
||||
LoadType.REFRESH -> {
|
||||
Pair<String?, String?>(null, null)
|
||||
}
|
||||
LoadType.PREPEND -> {
|
||||
//No prepend for the moment, might be nice to add later
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
LoadType.APPEND -> {
|
||||
Pair<String?, String?>(state.lastItemOrNull()?.id, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
|
||||
val accessToken = user.accessToken.orEmpty()
|
||||
|
||||
val apiResponse = api.timelineHome( "Bearer $accessToken",
|
||||
max_id= max_id, min_id = min_id,
|
||||
limit = state.config.pageSize.toString())
|
||||
|
||||
val dbObjects = apiResponse.map{
|
||||
HomeStatusDatabaseEntity(user.user_id, user.instance_uri, it)
|
||||
}
|
||||
|
||||
val endOfPaginationReached = apiResponse.isEmpty()
|
||||
|
||||
db.withTransaction {
|
||||
// clear table in the database
|
||||
if (loadType == LoadType.REFRESH) {
|
||||
db.homePostDao().clearFeedContent()
|
||||
}
|
||||
db.homePostDao().insertAll(dbObjects)
|
||||
}
|
||||
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
|
||||
} catch (exception: IOException) {
|
||||
return MediatorResult.Error(exception)
|
||||
} catch (exception: HttpException) {
|
||||
return MediatorResult.Error(exception)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package com.h.pixeldroid.fragments.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.paging.RemoteMediator
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.db.dao.feedContent.FeedContentDao
|
||||
import com.h.pixeldroid.fragments.StatusViewHolder
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.FeedViewModel
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.CachedFeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.cachedFeeds.ViewModelFactory
|
||||
import com.h.pixeldroid.objects.FeedContentDatabase
|
||||
import com.h.pixeldroid.objects.Status
|
||||
|
||||
|
||||
/**
|
||||
* Fragment for the home feed or public feed tabs.
|
||||
*
|
||||
* Takes a "home" boolean in its arguments [Bundle] to determine which
|
||||
*/
|
||||
@ExperimentalPagingApi
|
||||
class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
|
||||
|
||||
private lateinit var mediator: RemoteMediator<Int, T>
|
||||
private lateinit var dao: FeedContentDao<T>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = PostsAdapter()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (requireArguments().get("home") as Boolean){
|
||||
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
|
||||
dao = db.homePostDao() as FeedContentDao<T>
|
||||
}
|
||||
else {
|
||||
mediator = PublicFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
|
||||
dao = db.publicPostDao() as FeedContentDao<T>
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(this, ViewModelFactory(db, dao, mediator))
|
||||
.get(FeedViewModel::class.java) as FeedViewModel<T>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
inner class PostsAdapter : PagingDataAdapter<T, RecyclerView.ViewHolder>(
|
||||
object : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return StatusViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.post_fragment
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position) as Status
|
||||
uiModel.let {
|
||||
val instanceUri = db.userDao().getActiveUser()!!.instance_uri
|
||||
val accessToken = db.userDao().getActiveUser()!!.accessToken
|
||||
(holder as StatusViewHolder).bind(it, instanceUri, apiHolder.setDomain(instanceUri), "Bearer $accessToken")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.fragments.feeds.cachedFeeds.postFeeds
|
||||
|
||||
import androidx.paging.*
|
||||
import androidx.room.withTransaction
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.entities.PublicFeedStatusDatabaseEntity
|
||||
import com.h.pixeldroid.di.PixelfedAPIHolder
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* RemoteMediator for the public feed.
|
||||
*
|
||||
* A [RemoteMediator] defines a set of callbacks used to incrementally load data from a remote
|
||||
* source into a local source wrapped by a [PagingSource], e.g., loading data from network into
|
||||
* a local db cache.
|
||||
*/
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class PublicFeedRemoteMediator @Inject constructor(
|
||||
private val apiHolder: PixelfedAPIHolder,
|
||||
private val db: AppDatabase
|
||||
) : RemoteMediator<Int, PublicFeedStatusDatabaseEntity>() {
|
||||
|
||||
override suspend fun load(loadType: LoadType, state: PagingState<Int, PublicFeedStatusDatabaseEntity>): MediatorResult {
|
||||
|
||||
val (max_id, min_id) = when (loadType) {
|
||||
LoadType.REFRESH -> {
|
||||
Pair<String?, String?>(null, null)
|
||||
}
|
||||
LoadType.PREPEND -> {
|
||||
//No prepend for the moment, might be nice to add later
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
LoadType.APPEND -> {
|
||||
Pair<String?, String?>(state.lastItemOrNull()?.id, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
val user = db.userDao().getActiveUser()!!
|
||||
val api = apiHolder.api ?: apiHolder.setDomainToCurrentUser(db)
|
||||
|
||||
val apiResponse = api.timelinePublic(
|
||||
max_id = max_id,
|
||||
min_id = min_id,
|
||||
limit = state.config.pageSize.toString(),
|
||||
)
|
||||
|
||||
val dbObjects = apiResponse.map{
|
||||
PublicFeedStatusDatabaseEntity(user.user_id, user.instance_uri, it)
|
||||
}
|
||||
val endOfPaginationReached = apiResponse.isEmpty()
|
||||
|
||||
db.withTransaction {
|
||||
// clear table in the database
|
||||
if (loadType == LoadType.REFRESH) {
|
||||
db.publicPostDao().clearFeedContent()
|
||||
}
|
||||
db.publicPostDao().insertAll(dbObjects)
|
||||
}
|
||||
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
|
||||
} catch (exception: IOException) {
|
||||
return MediatorResult.Error(exception)
|
||||
} catch (exception: HttpException) {
|
||||
return MediatorResult.Error(exception)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds.postFeeds
|
||||
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.DBUtils
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class HomeTimelineFragment: PostsFeedFragment() {
|
||||
|
||||
override fun makeContent(): LiveData<PagedList<Status>> {
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
val dataSource = PostFeedDataSource()
|
||||
factory = FeedDataSourceFactory(dataSource)
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
|
||||
|
||||
inner class PostFeedDataSource: FeedDataSource<String, Status>() {
|
||||
|
||||
override fun newSource(): PostFeedDataSource {
|
||||
return PostFeedDataSource()
|
||||
}
|
||||
|
||||
override fun makeInitialCall(requestedLoadSize: Int): Call<List<Status>> {
|
||||
return pixelfedAPI
|
||||
.timelineHome("Bearer $accessToken", limit="$requestedLoadSize")
|
||||
}
|
||||
|
||||
override fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Status>> {
|
||||
return pixelfedAPI
|
||||
.timelineHome("Bearer $accessToken", max_id=key,
|
||||
limit="$requestedLoadSize")
|
||||
}
|
||||
|
||||
//We use the id as the key
|
||||
override fun getKey(item: Status): String {
|
||||
return item.id!!
|
||||
}
|
||||
|
||||
override fun enqueueCall(call: Call<List<Status>>, callback: LoadCallback<Status>){
|
||||
|
||||
call.enqueue(object : Callback<List<Status>> {
|
||||
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val notifications = response.body()!!
|
||||
callback.onResult(notifications)
|
||||
DBUtils.storePosts(db, notifications, user!!)
|
||||
} else {
|
||||
showError()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
loadingIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Status>>, t: Throwable) {
|
||||
showError(errorText = R.string.feed_failed)
|
||||
Log.e("PostsFeedFragment", t.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds.postFeeds
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import at.connyduck.sparkbutton.SparkButton
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.fragments.feeds.FeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.FeedsRecyclerViewAdapter
|
||||
import com.h.pixeldroid.objects.Status
|
||||
|
||||
abstract class PostsFeedFragment : FeedFragment() {
|
||||
|
||||
lateinit var picRequest: RequestBuilder<Drawable>
|
||||
lateinit var domain : String
|
||||
protected lateinit var adapter : FeedsRecyclerViewAdapter<Status, PostViewHolder>
|
||||
lateinit var factory: FeedDataSourceFactory<String, Status>
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
domain = user?.instance_uri.orEmpty()
|
||||
//RequestBuilder that is re-used for every image
|
||||
picRequest = Glide.with(this)
|
||||
.asDrawable().fitCenter()
|
||||
.placeholder(ColorDrawable(Color.GRAY))
|
||||
|
||||
adapter = PostsFeedRecyclerViewAdapter()
|
||||
list.adapter = adapter
|
||||
|
||||
//Make Glide be aware of the recyclerview and pre-load images
|
||||
val sizeProvider: ListPreloader.PreloadSizeProvider<Status> = ViewPreloadSizeProvider()
|
||||
val preloader: RecyclerViewPreloader<Status> = RecyclerViewPreloader(
|
||||
Glide.with(this), adapter as PostsFeedRecyclerViewAdapter, sizeProvider, 4
|
||||
)
|
||||
list.addOnScrollListener(preloader)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val content = makeContent()
|
||||
content.observe(viewLifecycleOwner,
|
||||
{ c ->
|
||||
adapter.submitList(c)
|
||||
//after a refresh is done we need to stop the pull to refresh spinner
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
})
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
showError(show = false)
|
||||
|
||||
//by invalidating data, loadInitial will be called again
|
||||
factory.liveData.value!!.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
internal abstract fun makeContent(): LiveData<PagedList<Status>>
|
||||
|
||||
/**
|
||||
* [RecyclerView.Adapter] that can display a list of Statuses
|
||||
*/
|
||||
inner class PostsFeedRecyclerViewAdapter
|
||||
: FeedsRecyclerViewAdapter<Status, PostViewHolder>(),
|
||||
ListPreloader.PreloadModelProvider<Status> {
|
||||
private val api = pixelfedAPI
|
||||
private val credential = "Bearer $accessToken"
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.post_fragment, parent, false)
|
||||
context = view.context
|
||||
return PostViewHolder(
|
||||
view,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the different elements of the Post Model to the view holder
|
||||
*/
|
||||
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
|
||||
val post = getItem(position) ?: return
|
||||
val metrics = context.resources.displayMetrics
|
||||
//Limit the height of the different images
|
||||
holder.postPic.maxHeight = metrics.heightPixels * 3/4
|
||||
|
||||
//Setup the post layout
|
||||
post.setupPost(holder.postView, picRequest, this@PostsFeedFragment, domain, false)
|
||||
|
||||
post.activateButtons(holder, api, credential)
|
||||
}
|
||||
|
||||
override fun getPreloadItems(position: Int): MutableList<Status> {
|
||||
val status = getItem(position) ?: return mutableListOf()
|
||||
return mutableListOf(status)
|
||||
}
|
||||
|
||||
override fun getPreloadRequestBuilder(item: Status): RequestBuilder<*>? {
|
||||
return picRequest.load(item.getPostUrl())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the posts that will be contained within the feed
|
||||
*/
|
||||
class PostViewHolder(val postView: View, val context: android.content.Context) : RecyclerView.ViewHolder(postView) {
|
||||
val profilePic : ImageView = postView.findViewById(R.id.profilePic)
|
||||
val postPic : ImageView = postView.findViewById(R.id.postPicture)
|
||||
val username : TextView = postView.findViewById(R.id.username)
|
||||
val usernameDesc: TextView = postView.findViewById(R.id.usernameDesc)
|
||||
val description : TextView = postView.findViewById(R.id.description)
|
||||
val nlikes : TextView = postView.findViewById(R.id.nlikes)
|
||||
val nshares : TextView = postView.findViewById(R.id.nshares)
|
||||
|
||||
//Spark buttons
|
||||
val liker : SparkButton = postView.findViewById(R.id.liker)
|
||||
val reblogger : SparkButton = postView.findViewById(R.id.reblogger)
|
||||
|
||||
val submitCmnt : ImageButton = postView.findViewById(R.id.submitComment)
|
||||
val commenter : ImageView = postView.findViewById(R.id.commenter)
|
||||
val comment : EditText = postView.findViewById(R.id.editComment)
|
||||
val commentCont : LinearLayout = postView.findViewById(R.id.commentContainer)
|
||||
val commentIn : LinearLayout = postView.findViewById(R.id.commentIn)
|
||||
val viewComment : TextView = postView.findViewById(R.id.ViewComments)
|
||||
val postDate : TextView = postView.findViewById(R.id.postDate)
|
||||
val postDomain : TextView = postView.findViewById(R.id.postDomain)
|
||||
val sensitiveW : TextView = postView.findViewById(R.id.sensitiveWarning)
|
||||
val postPager : ViewPager2 = postView.findViewById(R.id.postPager)
|
||||
|
||||
val more : ImageButton = postView.findViewById(R.id.status_more)
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds.postFeeds
|
||||
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class PublicTimelineFragment: PostsFeedFragment() {
|
||||
|
||||
inner class PublicFeedDataSource : FeedDataSource<String, Status>(){
|
||||
|
||||
override fun newSource(): PublicFeedDataSource {
|
||||
return PublicFeedDataSource()
|
||||
}
|
||||
|
||||
override fun makeInitialCall(requestedLoadSize: Int): Call<List<Status>> {
|
||||
return pixelfedAPI.timelinePublic(limit="$requestedLoadSize")
|
||||
}
|
||||
override fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Status>> {
|
||||
return pixelfedAPI.timelinePublic( max_id=key, limit="$requestedLoadSize")
|
||||
}
|
||||
|
||||
override fun enqueueCall(call: Call<List<Status>>, callback: LoadCallback<Status>) {
|
||||
call.enqueue(object : Callback<List<Status>> {
|
||||
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val notifications = response.body()!!
|
||||
callback.onResult(notifications)
|
||||
} else{
|
||||
showError()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
loadingIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Status>>, t: Throwable) {
|
||||
showError(errorText = R.string.feed_failed)
|
||||
Log.e("PublicTimelineFragment", t.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getKey(item: Status): String {
|
||||
return item.id!!
|
||||
}
|
||||
}
|
||||
|
||||
override fun makeContent(): LiveData<PagedList<Status>> {
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
factory = FeedDataSourceFactory(PublicFeedDataSource())
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds.search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.fragments.feeds.AccountListFragment
|
||||
import com.h.pixeldroid.fragments.feeds.FeedFragment
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Results
|
||||
import com.h.pixeldroid.objects.Tag
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class SearchAccountFragment: AccountListFragment(){
|
||||
|
||||
private lateinit var query: String
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
query = arguments?.getSerializable("searchFeed") as String
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
inner class SearchAccountListDataSource: FeedDataSource<String, Account>(){
|
||||
|
||||
override fun newSource(): SearchAccountListDataSource {
|
||||
return SearchAccountListDataSource()
|
||||
}
|
||||
|
||||
override fun getKey(item: Account): String {
|
||||
return content.value?.loadedCount.toString()
|
||||
}
|
||||
|
||||
private fun searchMakeInitialCall(requestedLoadSize: Int): Call<Results> {
|
||||
return pixelfedAPI
|
||||
.search("Bearer $accessToken",
|
||||
limit="$requestedLoadSize", q = query,
|
||||
type = Results.SearchType.accounts)
|
||||
}
|
||||
private fun searchMakeAfterCall(requestedLoadSize: Int, key: String): Call<Results> {
|
||||
return pixelfedAPI
|
||||
.search("Bearer $accessToken", offset = key,
|
||||
limit="$requestedLoadSize", q = query,
|
||||
type = Results.SearchType.accounts)
|
||||
}
|
||||
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<Account>
|
||||
) {
|
||||
searchEnqueueCall(searchMakeInitialCall(params.requestedLoadSize), callback)
|
||||
}
|
||||
|
||||
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Account>) {
|
||||
searchEnqueueCall(searchMakeAfterCall(params.requestedLoadSize, params.key), callback)
|
||||
}
|
||||
private fun searchEnqueueCall(call: Call<Results>, callback: LoadCallback<Account>) {
|
||||
|
||||
call.enqueue(object : Callback<Results> {
|
||||
override fun onResponse(call: Call<Results>, response: Response<Results>) {
|
||||
if (response.code() == 200) {
|
||||
val notifications = response.body()!!.accounts as ArrayList<Account>
|
||||
callback.onResult(notifications as List<Account>)
|
||||
} else{
|
||||
showError()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
loadingIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Results>, t: Throwable) {
|
||||
showError(errorText = R.string.feed_failed)
|
||||
Log.e("FeedFragment", t.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun makeInitialCall(requestedLoadSize: Int): Call<List<Account>> {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
|
||||
override fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Account>> {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
|
||||
override fun enqueueCall(call: Call<List<Account>>, callback: LoadCallback<Account>) {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
}
|
||||
|
||||
override fun makeContent(): LiveData<PagedList<Account>> {
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
factory = FeedFragment().FeedDataSourceFactory(SearchAccountListDataSource())
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds.search
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.fragments.feeds.AccountListFragment
|
||||
import com.h.pixeldroid.fragments.feeds.FeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.FeedsRecyclerViewAdapter
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Results
|
||||
import com.h.pixeldroid.objects.Tag
|
||||
import kotlinx.android.synthetic.main.fragment_tags.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class SearchHashtagFragment: FeedFragment(){
|
||||
|
||||
private lateinit var query: String
|
||||
private lateinit var content: LiveData<PagedList<Tag>>
|
||||
private lateinit var adapter : TagsRecyclerViewAdapter
|
||||
lateinit var factory: FeedDataSourceFactory<Int, Tag>
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
query = arguments?.getSerializable("searchFeed") as String
|
||||
|
||||
adapter = TagsRecyclerViewAdapter()
|
||||
list.adapter = adapter
|
||||
|
||||
return view
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
content = makeContent()
|
||||
|
||||
content.observe(viewLifecycleOwner,
|
||||
Observer { c ->
|
||||
adapter.submitList(c)
|
||||
//after a refresh is done we need to stop the pull to refresh spinner
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
})
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
showError(show = false)
|
||||
|
||||
//by invalidating data, loadInitial will be called again
|
||||
factory.liveData.value!!.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
inner class SearchTagsListDataSource: FeedDataSource<Int, Tag>(){
|
||||
|
||||
override fun newSource(): SearchTagsListDataSource {
|
||||
return SearchTagsListDataSource()
|
||||
}
|
||||
|
||||
private fun searchMakeInitialCall(requestedLoadSize: Int): Call<Results> {
|
||||
return pixelfedAPI
|
||||
.search("Bearer $accessToken",
|
||||
limit="$requestedLoadSize", q=query,
|
||||
type = Results.SearchType.hashtags)
|
||||
}
|
||||
private fun searchMakeAfterCall(requestedLoadSize: Int, key: Int): Call<Results> {
|
||||
return pixelfedAPI
|
||||
.search("Bearer $accessToken", offset = key.toString(),
|
||||
limit="$requestedLoadSize", q = query,
|
||||
type = Results.SearchType.hashtags)
|
||||
}
|
||||
|
||||
override fun getKey(item: Tag): Int {
|
||||
val value = content.value
|
||||
return value?.loadedCount ?: 0
|
||||
}
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<Int>,
|
||||
callback: LoadInitialCallback<Tag>
|
||||
) {
|
||||
searchEnqueueCall(searchMakeInitialCall(params.requestedLoadSize), callback)
|
||||
}
|
||||
|
||||
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Tag>) {
|
||||
searchEnqueueCall(searchMakeAfterCall(params.requestedLoadSize, params.key), callback)
|
||||
}
|
||||
|
||||
private fun searchEnqueueCall(call: Call<Results>, callback: LoadCallback<Tag>){
|
||||
|
||||
call.enqueue(object : Callback<Results> {
|
||||
override fun onResponse(call: Call<Results>, response: Response<Results>) {
|
||||
if (response.code() == 200) {
|
||||
val notifications = response.body()!!.hashtags as ArrayList<Tag>
|
||||
callback.onResult(notifications as List<Tag>)
|
||||
|
||||
} else{
|
||||
showError()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
loadingIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Results>, t: Throwable) {
|
||||
showError(errorText = R.string.feed_failed)
|
||||
Log.e("FeedFragment", t.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun makeInitialCall(requestedLoadSize: Int): Call<List<Tag>> {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
|
||||
override fun makeAfterCall(requestedLoadSize: Int, key: Int): Call<List<Tag>> {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
|
||||
override fun enqueueCall(call: Call<List<Tag>>, callback: LoadCallback<Tag>) {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeContent(): LiveData<PagedList<Tag>> {
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
factory =
|
||||
FeedFragment()
|
||||
.FeedDataSourceFactory(
|
||||
SearchTagsListDataSource()
|
||||
)
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
|
||||
inner class TagsRecyclerViewAdapter : FeedsRecyclerViewAdapter<Tag, TagsRecyclerViewAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.fragment_tags, parent, false)
|
||||
context = view.context
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder : ViewHolder, position : Int) {
|
||||
val tag = getItem(position) ?: return
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
holder.name.text = "#" + tag.name
|
||||
|
||||
holder.mView.setOnClickListener { Log.e("Tag: ", tag.name) }
|
||||
}
|
||||
|
||||
inner class ViewHolder(val mView : View) : RecyclerView.ViewHolder(mView) {
|
||||
val name : TextView = mView.tag_name
|
||||
}
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
package com.h.pixeldroid.fragments.feeds.search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.fragments.feeds.FeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.PostsFeedFragment
|
||||
import com.h.pixeldroid.objects.Results
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class SearchPostsFragment: PostsFeedFragment(){
|
||||
|
||||
private lateinit var query: String
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
query = arguments?.getSerializable("searchFeed") as String
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
inner class SearchFeedDataSource : FeedDataSource<String, Status>(){
|
||||
|
||||
override fun newSource(): SearchFeedDataSource {
|
||||
return SearchFeedDataSource()
|
||||
}
|
||||
|
||||
private fun searchMakeInitialCall(requestedLoadSize: Int): Call<Results> {
|
||||
return pixelfedAPI
|
||||
.search("Bearer $accessToken",
|
||||
limit="$requestedLoadSize", q=query,
|
||||
type = Results.SearchType.statuses)
|
||||
}
|
||||
private fun searchMakeAfterCall(requestedLoadSize: Int, key: String): Call<Results> {
|
||||
return pixelfedAPI
|
||||
.search("Bearer $accessToken", max_id=key,
|
||||
limit="$requestedLoadSize", q = query,
|
||||
type = Results.SearchType.statuses)
|
||||
}
|
||||
override fun loadInitial(
|
||||
params: LoadInitialParams<String>,
|
||||
callback: LoadInitialCallback<Status>
|
||||
) {
|
||||
searchEnqueueCall(searchMakeInitialCall(params.requestedLoadSize), callback)
|
||||
}
|
||||
|
||||
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Status>) {
|
||||
searchEnqueueCall(searchMakeAfterCall(params.requestedLoadSize, params.key), callback)
|
||||
}
|
||||
|
||||
private fun searchEnqueueCall(call: Call<Results>, callback: LoadCallback<Status>){
|
||||
|
||||
call.enqueue(object : Callback<Results> {
|
||||
override fun onResponse(call: Call<Results>, response: Response<Results>) {
|
||||
if (response.code() == 200) {
|
||||
val notifications = response.body()!!.statuses as ArrayList<Status>
|
||||
callback.onResult(notifications as List<Status>)
|
||||
} else {
|
||||
showError()
|
||||
}
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
loadingIndicator.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Results>, t: Throwable) {
|
||||
showError(errorText = R.string.feed_failed)
|
||||
Log.e("FeedFragment", t.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
override fun makeInitialCall(requestedLoadSize: Int): Call<List<Status>> {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
|
||||
override fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Status>> {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
|
||||
override fun enqueueCall(call: Call<List<Status>>, callback: LoadCallback<Status>) {
|
||||
throw NotImplementedError("Should not be called, reimplemented for Search fragment")
|
||||
}
|
||||
|
||||
override fun getKey(item: Status): String {
|
||||
return item.id!!
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun makeContent(): LiveData<PagedList<Status>> {
|
||||
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
|
||||
factory = FeedFragment()
|
||||
.FeedDataSourceFactory(SearchFeedDataSource())
|
||||
return LivePagedListBuilder(factory, config).build()
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.*
|
||||
import com.h.pixeldroid.objects.FeedContent
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* ViewModel for the uncached feeds.
|
||||
* The ViewModel works with the different [UncachedContentRepository]s to get the data.
|
||||
*/
|
||||
class FeedViewModel<T: FeedContent>(private val repository: UncachedContentRepository<T>) : ViewModel() {
|
||||
|
||||
private var currentResult: Flow<PagingData<T>>? = null
|
||||
|
||||
fun flow(): Flow<PagingData<T>> {
|
||||
val lastResult = currentResult
|
||||
if (lastResult != null) {
|
||||
return lastResult
|
||||
}
|
||||
val newResult: Flow<PagingData<T>> = repository.getStream()
|
||||
.cachedIn(viewModelScope)
|
||||
currentResult = newResult
|
||||
return newResult
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common interface for the different uncached feeds
|
||||
*/
|
||||
interface UncachedContentRepository<T: FeedContent>{
|
||||
fun getStream(): Flow<PagingData<T>>
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import com.h.pixeldroid.databinding.FragmentFeedBinding
|
||||
import com.h.pixeldroid.fragments.BaseFragment
|
||||
import com.h.pixeldroid.fragments.feeds.initAdapter
|
||||
import com.h.pixeldroid.objects.FeedContent
|
||||
|
||||
|
||||
/**
|
||||
* A fragment representing a list of [FeedContent], not backed by a db cache.
|
||||
*/
|
||||
open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
|
||||
|
||||
internal lateinit var viewModel: FeedViewModel<T>
|
||||
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
|
||||
|
||||
private lateinit var binding: FragmentFeedBinding
|
||||
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
|
||||
internal fun launch() {
|
||||
// Make sure we cancel the previous job before creating a new one
|
||||
job?.cancel()
|
||||
job = lifecycleScope.launch {
|
||||
viewModel.flow().collectLatest {
|
||||
adapter.submitData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun initSearch() {
|
||||
// Scroll to top when the list is refreshed from network.
|
||||
lifecycleScope.launch {
|
||||
adapter.loadStateFlow
|
||||
// Only emit when REFRESH LoadState for RemoteMediator changes.
|
||||
.distinctUntilChangedBy { it.refresh }
|
||||
// Only react to cases where Remote REFRESH completes i.e., NotLoading.
|
||||
.filter { it.refresh is LoadState.NotLoading }
|
||||
.collect { binding.list.scrollToPosition(0) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
binding = FragmentFeedBinding.inflate(layoutInflater)
|
||||
|
||||
initAdapter(binding, adapter)
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
//It shouldn't be necessary to also retry() in addition to refresh(),
|
||||
//but if we don't do this, reloads after an error fail immediately...
|
||||
adapter.retry()
|
||||
adapter.refresh()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
class ViewModelFactory<U: FeedContent> @ExperimentalPagingApi constructor(
|
||||
private val searchContentRepository: UncachedContentRepository<U>
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(FeedViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return FeedViewModel(searchContentRepository) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds.accountLists
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.FeedViewModel
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.UncachedFeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.ViewModelFactory
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Account.Companion.ACCOUNT_ID_TAG
|
||||
import com.h.pixeldroid.objects.Account.Companion.FOLLOWERS_TAG
|
||||
import kotlinx.android.synthetic.main.account_list_entry.view.*
|
||||
|
||||
|
||||
/**
|
||||
* Fragment to show a list of [Account]s, for a list of followers or following
|
||||
*/
|
||||
class AccountListFragment : UncachedFeedFragment<Account>() {
|
||||
|
||||
private lateinit var id: String
|
||||
private var following: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
id = arguments?.getSerializable(ACCOUNT_ID_TAG) as String
|
||||
following = arguments?.getSerializable(FOLLOWERS_TAG) as Boolean
|
||||
|
||||
adapter = AccountAdapter()
|
||||
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(this, ViewModelFactory(
|
||||
FollowersContentRepository(
|
||||
apiHolder.setDomainToCurrentUser(db),
|
||||
db.userDao().getActiveUser()!!.accessToken,
|
||||
id,
|
||||
following
|
||||
)
|
||||
)
|
||||
)
|
||||
.get(FeedViewModel::class.java) as FeedViewModel<Account>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* View Holder for an [Account] RecyclerView list item.
|
||||
*/
|
||||
class AccountViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val avatar : ImageView = view.account_entry_avatar
|
||||
private val username : TextView = view.account_entry_username
|
||||
private val acct: TextView = view.account_entry_acct
|
||||
|
||||
private var account: Account? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
account?.openProfile(itemView.context)
|
||||
}
|
||||
}
|
||||
|
||||
fun bind(account: Account?) {
|
||||
|
||||
this.account = account
|
||||
|
||||
Glide.with(itemView)
|
||||
.load(account?.avatar_static ?: account?.avatar)
|
||||
.circleCrop().placeholder(R.drawable.ic_default_user)
|
||||
.into(avatar)
|
||||
|
||||
username.text = account?.username
|
||||
@SuppressLint("SetTextI18n")
|
||||
acct.text = "@${account?.acct}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup): AccountViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.account_list_entry, parent, false)
|
||||
return AccountViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
class AccountAdapter : PagingDataAdapter<Account, RecyclerView.ViewHolder>(
|
||||
UIMODEL_COMPARATOR
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return AccountViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.account_list_entry
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position)
|
||||
uiModel.let {
|
||||
(holder as AccountViewHolder).bind(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val UIMODEL_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
|
||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds.accountLists
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.UncachedContentRepository
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class FollowersContentRepository @ExperimentalPagingApi
|
||||
@Inject constructor(
|
||||
private val api: PixelfedAPI,
|
||||
private val accessToken: String,
|
||||
private val accountId: String,
|
||||
private val following: Boolean,
|
||||
): UncachedContentRepository<Account> {
|
||||
override fun getStream(): Flow<PagingData<Account>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
initialLoadSize = NETWORK_PAGE_SIZE,
|
||||
pageSize = NETWORK_PAGE_SIZE,
|
||||
enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
FollowersPagingSource(api, accessToken, accountId, following)
|
||||
}
|
||||
).flow
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NETWORK_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds.accountLists
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class FollowersPagingSource(
|
||||
private val api: PixelfedAPI,
|
||||
private val accessToken: String,
|
||||
private val accountId: String,
|
||||
private val following: Boolean
|
||||
) : PagingSource<Int, Account>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Account> {
|
||||
val position = params.key
|
||||
return try {
|
||||
val response =
|
||||
// Pixelfed and Mastodon don't implement this in the same fashion. Pixelfed uses
|
||||
// Laravel's paging mechanism, while Mastodon uses the Link header for pagination.
|
||||
// No need to know which is which, they should ignore the non-relevant argument
|
||||
if(following) {
|
||||
api.followers(account_id = accountId,
|
||||
authorization = "Bearer $accessToken",
|
||||
limit = params.loadSize,
|
||||
page = position?.toString(),
|
||||
max_id = position?.toString())
|
||||
} else {
|
||||
api.following(account_id = accountId,
|
||||
authorization = "Bearer $accessToken",
|
||||
limit = params.loadSize,
|
||||
page = position?.toString(),
|
||||
max_id = position?.toString())
|
||||
}
|
||||
|
||||
val accounts = if(response.isSuccessful){
|
||||
response.body().orEmpty()
|
||||
} else {
|
||||
throw HttpException(response)
|
||||
}
|
||||
|
||||
val nextPosition = if(response.headers()["Link"] != null){
|
||||
//Header is of the form:
|
||||
// Link: <https://mastodon.social/api/v1/accounts/1/followers?limit=2&max_id=7628164>; rel="next", <https://mastodon.social/api/v1/accounts/1/followers?limit=2&since_id=7628165>; 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
|
||||
} else {
|
||||
params.key?.plus(1) ?: 2
|
||||
}
|
||||
|
||||
LoadResult.Page(
|
||||
data = accounts,
|
||||
prevKey = null,
|
||||
nextKey = if (accounts.isEmpty()) null else nextPosition
|
||||
)
|
||||
} catch (exception: IOException) {
|
||||
LoadResult.Error(exception)
|
||||
} catch (exception: HttpException) {
|
||||
LoadResult.Error(exception)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds.search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.*
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.accountLists.AccountAdapter
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Results
|
||||
|
||||
/**
|
||||
* Fragment to show a list of [Account]s, as a result of a search.
|
||||
*/
|
||||
class SearchAccountFragment : UncachedFeedFragment<Account>() {
|
||||
|
||||
private lateinit var query: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = AccountAdapter()
|
||||
|
||||
query = arguments?.getSerializable("searchFeed") as String
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(this, ViewModelFactory(
|
||||
SearchContentRepository<Account>(
|
||||
apiHolder.setDomainToCurrentUser(db),
|
||||
Results.SearchType.accounts,
|
||||
db.userDao().getActiveUser()!!.accessToken,
|
||||
query
|
||||
)
|
||||
)
|
||||
)
|
||||
.get(FeedViewModel::class.java) as FeedViewModel<Account>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds.search
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.UncachedContentRepository
|
||||
import com.h.pixeldroid.objects.FeedContent
|
||||
import com.h.pixeldroid.objects.Results
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Repository class to perform searches
|
||||
*
|
||||
* The type argument [T] and the [Results.SearchType][type] argument should always
|
||||
* be in agreement, e.g. if [T] is a [com.h.pixeldroid.objects.Account] then
|
||||
* [type] should be [Results.SearchType.accounts].
|
||||
*/
|
||||
class SearchContentRepository<T: FeedContent> @ExperimentalPagingApi
|
||||
@Inject constructor(
|
||||
private val api: PixelfedAPI,
|
||||
private val type: Results.SearchType,
|
||||
private val accessToken: String,
|
||||
private val query: String,
|
||||
): UncachedContentRepository<T> {
|
||||
override fun getStream(): Flow<PagingData<T>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
initialLoadSize = NETWORK_PAGE_SIZE,
|
||||
pageSize = NETWORK_PAGE_SIZE,
|
||||
enablePlaceholders = false),
|
||||
pagingSourceFactory = {
|
||||
SearchPagingSource<T>(api, query, type, accessToken)
|
||||
}
|
||||
).flow
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NETWORK_PAGE_SIZE = 20
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds.search
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.UncachedFeedFragment
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.FeedViewModel
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.ViewModelFactory
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Results
|
||||
import com.h.pixeldroid.objects.Tag
|
||||
import kotlinx.android.synthetic.main.fragment_tags.view.*
|
||||
|
||||
/**
|
||||
* Fragment to show a list of [hashtag][Tag]s, as a result of a search.
|
||||
*/
|
||||
class SearchHashtagFragment : UncachedFeedFragment<Tag>() {
|
||||
|
||||
private lateinit var query: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
adapter = HashTagAdapter()
|
||||
|
||||
query = arguments?.getSerializable("searchFeed") as String
|
||||
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(this, ViewModelFactory(
|
||||
SearchContentRepository<Tag>(
|
||||
apiHolder.setDomainToCurrentUser(db),
|
||||
Results.SearchType.hashtags,
|
||||
db.userDao().getActiveUser()!!.accessToken,
|
||||
query
|
||||
)
|
||||
)
|
||||
)
|
||||
.get(FeedViewModel::class.java) as FeedViewModel<Tag>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
class HashTagAdapter : PagingDataAdapter<Tag, RecyclerView.ViewHolder>(
|
||||
UIMODEL_COMPARATOR
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return HashTagViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.fragment_tags
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position)
|
||||
uiModel.let {
|
||||
(holder as HashTagViewHolder).bind(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val UIMODEL_COMPARATOR = object : DiffUtil.ItemCallback<Tag>() {
|
||||
override fun areItemsTheSame(oldItem: Tag, newItem: Tag): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Tag, newItem: Tag): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* View Holder for a [Tag] RecyclerView list item.
|
||||
*/
|
||||
class HashTagViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
private val name : TextView = view.tag_name
|
||||
|
||||
private var tag: Tag? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun bind(tag: Tag?) {
|
||||
|
||||
this.tag = tag
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
name.text = "#" + tag?.name
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(parent: ViewGroup): HashTagViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.fragment_tags, parent, false)
|
||||
return HashTagViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds.search
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.objects.FeedContent
|
||||
import com.h.pixeldroid.objects.Results
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Provides the PagingSource for search feeds. Is used in [SearchContentRepository]
|
||||
*/
|
||||
class SearchPagingSource<T: FeedContent>(
|
||||
private val api: PixelfedAPI,
|
||||
private val query: String,
|
||||
private val type: Results.SearchType,
|
||||
private val accessToken: String,
|
||||
) : PagingSource<Int, T>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
|
||||
val position = params.key
|
||||
return try {
|
||||
val response = api.search(authorization = "Bearer $accessToken",
|
||||
offset = position?.toString(),
|
||||
q = query,
|
||||
type = type,
|
||||
limit = params.loadSize.toString())
|
||||
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val repos = when(type){
|
||||
Results.SearchType.accounts -> response.accounts
|
||||
Results.SearchType.hashtags -> response.hashtags
|
||||
Results.SearchType.statuses -> response.statuses
|
||||
} as List<T>
|
||||
|
||||
LoadResult.Page(
|
||||
data = repos,
|
||||
prevKey = null,
|
||||
nextKey = if (repos.isEmpty()) null else (position ?: 0) + repos.size
|
||||
)
|
||||
} catch (exception: IOException) {
|
||||
LoadResult.Error(exception)
|
||||
} catch (exception: HttpException) {
|
||||
LoadResult.Error(exception)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package com.h.pixeldroid.fragments.feeds.uncachedFeeds.search
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.fragments.StatusViewHolder
|
||||
import com.h.pixeldroid.fragments.feeds.uncachedFeeds.*
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Results
|
||||
import com.h.pixeldroid.objects.Status
|
||||
|
||||
/**
|
||||
* Fragment to show a list of [Status]es, as a result of a search.
|
||||
*/
|
||||
class SearchPostsFragment : UncachedFeedFragment<Status>() {
|
||||
|
||||
private lateinit var query: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
adapter = PostsAdapter()
|
||||
|
||||
query = arguments?.getSerializable("searchFeed") as String
|
||||
|
||||
}
|
||||
|
||||
@ExperimentalPagingApi
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
// get the view model
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
viewModel = ViewModelProvider(this, ViewModelFactory(
|
||||
SearchContentRepository<Status>(
|
||||
apiHolder.setDomainToCurrentUser(db),
|
||||
Results.SearchType.statuses,
|
||||
db.userDao().getActiveUser()!!.accessToken,
|
||||
query
|
||||
)
|
||||
)
|
||||
)
|
||||
.get(FeedViewModel::class.java) as FeedViewModel<Status>
|
||||
|
||||
launch()
|
||||
initSearch()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
inner class PostsAdapter : PagingDataAdapter<Status, RecyclerView.ViewHolder>(
|
||||
object : DiffUtil.ItemCallback<Status>() {
|
||||
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return StatusViewHolder.create(parent)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.layout.post_fragment
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val uiModel = getItem(position) as Status
|
||||
uiModel.let {
|
||||
val instanceUri = db.userDao().getActiveUser()!!.instance_uri
|
||||
val accessToken = db.userDao().getActiveUser()!!.accessToken
|
||||
(holder as StatusViewHolder).bind(it, instanceUri, apiHolder.setDomain(instanceUri), "Bearer $accessToken")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -41,8 +41,8 @@ data class Account(
|
||||
val moved: Account? = null,
|
||||
val fields: List<Field>? = emptyList(),
|
||||
val bot: Boolean? = false,
|
||||
val source: Source? = null
|
||||
) : Serializable, FeedContent() {
|
||||
val source: Source? = null,
|
||||
) : Serializable, FeedContent {
|
||||
companion object {
|
||||
const val ACCOUNT_TAG = "AccountTag"
|
||||
const val ACCOUNT_ID_TAG = "AccountIdTag"
|
||||
|
@ -1,10 +1,13 @@
|
||||
package com.h.pixeldroid.objects
|
||||
|
||||
abstract class FeedContent {
|
||||
abstract val id: String?
|
||||
interface FeedContent {
|
||||
val id: String?
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
interface FeedContentDatabase {
|
||||
val id: String?
|
||||
|
||||
var user_id: String
|
||||
|
||||
var instance_uri: String
|
||||
}
|
@ -1,11 +1,28 @@
|
||||
package com.h.pixeldroid.objects
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import java.io.Serializable
|
||||
import java.util.Date
|
||||
|
||||
/*
|
||||
Represents a notification of an event relevant to the user.
|
||||
https://docs.joinmastodon.org/entities/notification/
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "notifications",
|
||||
primaryKeys = ["id", "user_id", "instance_uri"],
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = UserDatabaseEntity::class,
|
||||
parentColumns = arrayOf("user_id", "instance_uri"),
|
||||
childColumns = arrayOf("user_id", "instance_uri"),
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
indices = [Index(value = ["user_id", "instance_uri"])]
|
||||
)
|
||||
data class Notification(
|
||||
//Required attributes
|
||||
override val id: String,
|
||||
@ -13,9 +30,14 @@ data class Notification(
|
||||
val created_at: Date, //ISO 8601 Datetime
|
||||
val account: Account,
|
||||
//Optional attributes
|
||||
val status: Status? = null
|
||||
): FeedContent() {
|
||||
enum class NotificationType {
|
||||
val status: Status? = null,
|
||||
|
||||
//Database values (not from API)
|
||||
//TODO do we find this approach acceptable? Preferable to a semi-duplicate NotificationDataBaseEntity?
|
||||
override var user_id: String,
|
||||
override var instance_uri: String,
|
||||
): FeedContent, FeedContentDatabase {
|
||||
enum class NotificationType: Serializable {
|
||||
follow, mention, reblog, favourite, poll
|
||||
}
|
||||
}
|
@ -1,63 +1,43 @@
|
||||
package com.h.pixeldroid.objects
|
||||
|
||||
import android.Manifest
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.widget.*
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.ReportActivity
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.fragments.ImageFragment
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.PostViewHolder
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.utils.HtmlUtils.Companion.getDomain
|
||||
import com.h.pixeldroid.utils.HtmlUtils.Companion.parseHTMLText
|
||||
import com.h.pixeldroid.utils.ImageConverter
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.censorColorMatrix
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.likePostCall
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.postComment
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.reblogPost
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.retrieveComments
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.toggleCommentInput
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.unLikePostCall
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.uncensorColorMatrix
|
||||
import com.h.pixeldroid.utils.PostUtils.Companion.undoReblogPost
|
||||
import com.h.pixeldroid.utils.Utils.Companion.setTextViewFromISO8601
|
||||
import com.karumi.dexter.Dexter
|
||||
import com.karumi.dexter.listener.PermissionDeniedResponse
|
||||
import com.karumi.dexter.listener.PermissionGrantedResponse
|
||||
import com.karumi.dexter.listener.single.BasePermissionListener
|
||||
import kotlinx.android.synthetic.main.post_fragment.view.*
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/*
|
||||
Represents a status posted by an account.
|
||||
https://docs.joinmastodon.org/entities/status/
|
||||
/**
|
||||
Represents a status posted by an account.
|
||||
https://docs.joinmastodon.org/entities/status/
|
||||
*/
|
||||
data class Status(
|
||||
|
||||
open class Status(
|
||||
//Base attributes
|
||||
override val id: String?,
|
||||
override val id: String,
|
||||
val uri: String? = "",
|
||||
val created_at: Date? = Date(0), //ISO 8601 Datetime
|
||||
val account: Account?,
|
||||
@ -89,10 +69,9 @@ data class Status(
|
||||
val reblogged: Boolean? = false,
|
||||
val muted: Boolean? = false,
|
||||
val bookmarked: Boolean? = false,
|
||||
val pinned: Boolean? = false
|
||||
) : Serializable, FeedContent()
|
||||
val pinned: Boolean? = false,
|
||||
) : Serializable, FeedContent
|
||||
{
|
||||
|
||||
companion object {
|
||||
const val POST_TAG = "postTag"
|
||||
const val DOMAIN_TAG = "domainTag"
|
||||
@ -117,45 +96,14 @@ data class Status(
|
||||
return context.getString(R.string.shares).format(reblogs_count.toString())
|
||||
}
|
||||
|
||||
private fun getStatusDomain(domain: String) : String {
|
||||
fun getStatusDomain(domain: String) : String {
|
||||
val accountDomain = getDomain(account!!.url)
|
||||
return if(getDomain(domain) == accountDomain) ""
|
||||
else " from $accountDomain"
|
||||
|
||||
}
|
||||
|
||||
private fun setupPostPics(
|
||||
rootView: View,
|
||||
request: RequestBuilder<Drawable>,
|
||||
homeFragment: Fragment
|
||||
) {
|
||||
|
||||
// Standard layout
|
||||
rootView.postPicture.visibility = VISIBLE
|
||||
rootView.postPager.visibility = GONE
|
||||
rootView.postTabs.visibility = GONE
|
||||
|
||||
|
||||
if(media_attachments?.size == 1) {
|
||||
request.load(this.getPostUrl()).into(rootView.postPicture)
|
||||
val imgDescription = media_attachments[0].description.orEmpty().ifEmpty { rootView.context.getString(R.string.no_description) }
|
||||
rootView.postPicture.contentDescription = imgDescription
|
||||
|
||||
rootView.postPicture.setOnLongClickListener {
|
||||
Snackbar.make(it, imgDescription, Snackbar.LENGTH_SHORT).show()
|
||||
true
|
||||
}
|
||||
|
||||
} else if(media_attachments?.size!! > 1) {
|
||||
setupTabsLayout(rootView, request, homeFragment)
|
||||
}
|
||||
|
||||
if (sensitive!!) {
|
||||
setupSensitiveLayout(rootView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSensitiveLayout(view: View) {
|
||||
fun setupSensitiveLayout(view: View) {
|
||||
|
||||
// Set dark layout and warning message
|
||||
view.sensitiveWarning.visibility = VISIBLE
|
||||
@ -176,161 +124,6 @@ data class Status(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupTabsLayout(
|
||||
rootView: View,
|
||||
request: RequestBuilder<Drawable>,
|
||||
homeFragment: Fragment
|
||||
) {
|
||||
//Only show the viewPager and tabs
|
||||
rootView.postPicture.visibility = GONE
|
||||
rootView.postPager.visibility = VISIBLE
|
||||
rootView.postTabs.visibility = VISIBLE
|
||||
|
||||
val tabs : ArrayList<ImageFragment> = ArrayList()
|
||||
|
||||
//Fill the tabs with each mediaAttachment
|
||||
for(media in media_attachments!!) {
|
||||
tabs.add(ImageFragment.newInstance(media.url!!, media.description.orEmpty()))
|
||||
}
|
||||
|
||||
setupTabs(tabs, rootView, homeFragment)
|
||||
}
|
||||
|
||||
|
||||
private fun setupTabs(tabs: ArrayList<ImageFragment>, rootView: View, homeFragment: Fragment) {
|
||||
//Attach the given tabs to the view pager
|
||||
rootView.postPager.adapter = object : FragmentStateAdapter(homeFragment) {
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return tabs[position]
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return media_attachments?.size ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
TabLayoutMediator(rootView.postTabs, rootView.postPager) { tab, _ ->
|
||||
tab.icon = rootView.context.getDrawable(R.drawable.ic_dot_blue_12dp)
|
||||
}.attach()
|
||||
}
|
||||
|
||||
fun setupPost(
|
||||
rootView: View,
|
||||
request: RequestBuilder<Drawable>,
|
||||
homeFragment: Fragment,
|
||||
domain: String,
|
||||
isActivity: Boolean
|
||||
) {
|
||||
//Setup username as a button that opens the profile
|
||||
rootView.findViewById<TextView>(R.id.username).apply {
|
||||
text = this@Status.account?.getDisplayName() ?: ""
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
setOnClickListener { account?.openProfile(rootView.context) }
|
||||
}
|
||||
|
||||
rootView.findViewById<TextView>(R.id.usernameDesc).apply {
|
||||
text = this@Status.account?.getDisplayName() ?: ""
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
|
||||
rootView.findViewById<TextView>(R.id.nlikes).apply {
|
||||
text = this@Status.getNLikes(rootView.context)
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
|
||||
rootView.findViewById<TextView>(R.id.nshares).apply {
|
||||
text = this@Status.getNShares(rootView.context)
|
||||
setTypeface(null, Typeface.BOLD)
|
||||
}
|
||||
|
||||
//Convert the date to a readable string
|
||||
setTextViewFromISO8601(created_at!!, rootView.postDate, isActivity, rootView.context)
|
||||
|
||||
rootView.postDomain.text = getStatusDomain(domain)
|
||||
|
||||
//Setup images
|
||||
ImageConverter.setRoundImageFromURL(
|
||||
rootView,
|
||||
this.getProfilePicUrl(),
|
||||
rootView.profilePic
|
||||
)
|
||||
rootView.profilePic.setOnClickListener { account?.openProfile(rootView.context) }
|
||||
|
||||
//Setup post pic only if there are media attachments
|
||||
if(!media_attachments.isNullOrEmpty()) {
|
||||
setupPostPics(rootView, request, homeFragment)
|
||||
} else {
|
||||
rootView.postPicture.visibility = GONE
|
||||
rootView.postPager.visibility = GONE
|
||||
rootView.postTabs.visibility = GONE
|
||||
}
|
||||
|
||||
|
||||
//Set comment initial visibility
|
||||
rootView.findViewById<LinearLayout>(R.id.commentIn).visibility = GONE
|
||||
rootView.findViewById<LinearLayout>(R.id.commentContainer).visibility = GONE
|
||||
}
|
||||
|
||||
fun setDescription(rootView: View, api: PixelfedAPI, credential: String) {
|
||||
rootView.findViewById<TextView>(R.id.description).apply {
|
||||
if (content.isNullOrBlank()) {
|
||||
visibility = GONE
|
||||
} else {
|
||||
text = parseHTMLText(content, mentions, api, rootView.context, credential)
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun activateButtons(holder: PostViewHolder, api: PixelfedAPI, credential: String){
|
||||
|
||||
//Set the special HTML text
|
||||
setDescription(holder.postView, api, credential)
|
||||
|
||||
//Activate onclickListeners
|
||||
activateLiker(
|
||||
holder, api, credential,
|
||||
this.favourited ?: false
|
||||
)
|
||||
activateReblogger(
|
||||
holder, api, credential,
|
||||
this.reblogged ?: false
|
||||
)
|
||||
activateCommenter(holder, api, credential)
|
||||
|
||||
showComments(holder, api, credential)
|
||||
|
||||
//Activate double tap liking
|
||||
activateDoubleTapLiker(holder, api, credential)
|
||||
|
||||
activateMoreButton(holder)
|
||||
}
|
||||
|
||||
fun activateReblogger(
|
||||
holder: PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
isReblogged: Boolean
|
||||
) {
|
||||
holder.reblogger.apply {
|
||||
//Set initial button state
|
||||
isChecked = isReblogged
|
||||
|
||||
//Activate the button
|
||||
setEventListener { _, buttonState ->
|
||||
if (buttonState) {
|
||||
// Button is active
|
||||
undoReblogPost(holder, api, credential, this@Status)
|
||||
} else {
|
||||
// Button is inactive
|
||||
reblogPost(holder, api, credential, this@Status)
|
||||
}
|
||||
//show animation or not?
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadImage(context: Context, url: String, view: View, share: Boolean = false) {
|
||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
|
||||
@ -403,199 +196,7 @@ data class Status(
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun activateMoreButton(holder: PostViewHolder){
|
||||
holder.more.setOnClickListener {
|
||||
PopupMenu(it.context, it).apply {
|
||||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.post_more_menu_report -> {
|
||||
val intent = Intent(it.context, ReportActivity::class.java)
|
||||
intent.putExtra(POST_TAG, this@Status)
|
||||
startActivity(it.context, intent, null)
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_share_link -> {
|
||||
val share = Intent.createChooser(Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, uri)
|
||||
|
||||
type = "text/plain"
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, content)
|
||||
}, null)
|
||||
startActivity(it.context, share, null)
|
||||
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_save_to_gallery -> {
|
||||
Dexter.withContext(holder.context)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.withListener(object : BasePermissionListener() {
|
||||
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
|
||||
Toast.makeText(
|
||||
holder.context,
|
||||
holder.context.getString(R.string.write_permission_download_pic),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
|
||||
downloadImage(
|
||||
holder.context,
|
||||
media_attachments?.get(holder.postPager.currentItem)?.url
|
||||
?: "",
|
||||
holder.postView
|
||||
)
|
||||
}
|
||||
}).check()
|
||||
true
|
||||
}
|
||||
R.id.post_more_menu_share_picture -> {
|
||||
Dexter.withContext(holder.context)
|
||||
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.withListener(object : BasePermissionListener() {
|
||||
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
|
||||
Toast.makeText(
|
||||
holder.context,
|
||||
holder.context.getString(R.string.write_permission_share_pic),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
override fun onPermissionGranted(p0: PermissionGrantedResponse?) {
|
||||
downloadImage(
|
||||
holder.context,
|
||||
media_attachments?.get(holder.postPager.currentItem)?.url
|
||||
?: "",
|
||||
holder.postView,
|
||||
share = true,
|
||||
)
|
||||
}
|
||||
}).check()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
inflate(R.menu.post_more_menu)
|
||||
if(media_attachments.isNullOrEmpty()) {
|
||||
//make sure to disable image-related things if there aren't any
|
||||
menu.setGroupVisible(R.id.post_more_group_picture, false)
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun activateDoubleTapLiker(
|
||||
holder: PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String
|
||||
) {
|
||||
holder.apply {
|
||||
var clicked = false
|
||||
postPic.setOnClickListener {
|
||||
//Check that the post isn't hidden
|
||||
if(sensitiveW.visibility == GONE) {
|
||||
//Check for double click
|
||||
if(clicked) {
|
||||
if (holder.liker.isChecked) {
|
||||
// Button is active, unlike
|
||||
holder.liker.isChecked = false
|
||||
unLikePostCall(holder, api, credential, this@Status)
|
||||
} else {
|
||||
// Button is inactive, like
|
||||
holder.liker.playAnimation()
|
||||
holder.liker.isChecked = true
|
||||
likePostCall(holder, api, credential, this@Status)
|
||||
}
|
||||
} else {
|
||||
clicked = true
|
||||
|
||||
//Reset clicked to false after 500ms
|
||||
postPic.handler.postDelayed(fun() { clicked = false }, 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun activateLiker(
|
||||
holder: PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
isLiked: Boolean
|
||||
) {
|
||||
|
||||
holder.liker.apply {
|
||||
//Set initial state
|
||||
isChecked = isLiked
|
||||
|
||||
//Activate the liker
|
||||
setEventListener { _, buttonState ->
|
||||
if (buttonState) {
|
||||
// Button is active, unlike
|
||||
unLikePostCall(holder, api, credential, this@Status)
|
||||
} else {
|
||||
// Button is inactive, like
|
||||
likePostCall(holder, api, credential, this@Status)
|
||||
}
|
||||
//show animation or not?
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun showComments(
|
||||
holder: PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String
|
||||
) {
|
||||
//Show all comments of a post
|
||||
if (replies_count == 0) {
|
||||
holder.viewComment.text = holder.context.getString(R.string.NoCommentsToShow)
|
||||
} else {
|
||||
holder.viewComment.apply {
|
||||
text = "$replies_count ${holder.context.getString(R.string.CommentDisplay)}"
|
||||
setOnClickListener {
|
||||
visibility = GONE
|
||||
|
||||
//Retrieve the comments
|
||||
retrieveComments(holder, api, credential, this@Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun activateCommenter(
|
||||
holder: PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String
|
||||
) {
|
||||
//Toggle comment button
|
||||
toggleCommentInput(holder)
|
||||
|
||||
//Activate commenterpostPicture
|
||||
holder.submitCmnt.setOnClickListener {
|
||||
val textIn = holder.comment.text
|
||||
//Open text input
|
||||
if(textIn.isNullOrEmpty()) {
|
||||
Toast.makeText(
|
||||
holder.context,
|
||||
holder.context.getString(R.string.empty_comment),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
|
||||
//Post the comment
|
||||
postComment(holder, api, credential, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Visibility : Serializable {
|
||||
enum class Visibility: Serializable {
|
||||
public, unlisted, private, direct
|
||||
}
|
||||
}
|
@ -7,10 +7,9 @@ data class Tag(
|
||||
val name: String,
|
||||
val url: String,
|
||||
//Optional attributes
|
||||
val history: List<History>? = emptyList()) : Serializable, FeedContent() {
|
||||
val history: List<History>? = emptyList()) : Serializable, FeedContent {
|
||||
//needed to be a FeedContent, this inheritance is a bit fickle. Do not use.
|
||||
override val id: String
|
||||
get() = "tag"
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,11 @@
|
||||
package com.h.pixeldroid.utils
|
||||
|
||||
import com.h.pixeldroid.db.AppDatabase
|
||||
import com.h.pixeldroid.db.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.PostDatabaseEntity
|
||||
import com.h.pixeldroid.db.UserDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.InstanceDatabaseEntity
|
||||
import com.h.pixeldroid.db.entities.UserDatabaseEntity
|
||||
import com.h.pixeldroid.objects.Account
|
||||
import com.h.pixeldroid.objects.Instance
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.Utils.Companion.normalizeDomain
|
||||
import java.util.Date
|
||||
|
||||
class DBUtils {
|
||||
companion object {
|
||||
@ -48,40 +45,5 @@ class DBUtils {
|
||||
)
|
||||
db.instanceDao().insertInstance(dbInstance)
|
||||
}
|
||||
|
||||
fun storePosts(
|
||||
db: AppDatabase,
|
||||
data: List<*>,
|
||||
user: UserDatabaseEntity
|
||||
) {
|
||||
val dao = db.postDao()
|
||||
data.forEach { post ->
|
||||
if (post is Status
|
||||
&& !post.media_attachments.isNullOrEmpty()
|
||||
&& dao.count(post.uri ?: "", user.user_id, user.instance_uri) == 0) {
|
||||
val nPosts = dao.numberOfPosts(user.user_id, user.instance_uri) - MAX_NUMBER_OF_STORED_POSTS
|
||||
if (nPosts > 0) {
|
||||
dao.removeOlderPosts(nPosts)
|
||||
}
|
||||
dao.insertPost(PostDatabaseEntity(
|
||||
user_id = user.user_id,
|
||||
instance_uri = user.instance_uri,
|
||||
uri = post.uri ?: "",
|
||||
account_profile_picture = post.getProfilePicUrl() ?: "",
|
||||
account_name = (post.account?.getDisplayName() ?: "").toString(),
|
||||
media_urls = post.media_attachments.map {
|
||||
attachment -> attachment.url ?: ""
|
||||
},
|
||||
favourite_count = post.favourites_count ?: 0,
|
||||
reply_count = post.replies_count ?: 0,
|
||||
share_count = post.reblogs_count ?: 0,
|
||||
description = post.content ?: "",
|
||||
date = post.created_at ?: Date(0),
|
||||
likes = post.favourites_count ?: 0,
|
||||
shares = post.reblogs_count ?: 0
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,240 +1,9 @@
|
||||
package com.h.pixeldroid.utils
|
||||
|
||||
import android.graphics.ColorMatrix
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import com.h.pixeldroid.R
|
||||
import com.h.pixeldroid.api.PixelfedAPI
|
||||
import com.h.pixeldroid.fragments.feeds.postFeeds.PostViewHolder
|
||||
import com.h.pixeldroid.objects.Context
|
||||
import com.h.pixeldroid.objects.Status
|
||||
import com.h.pixeldroid.utils.ImageConverter.Companion.setImageFromDrawable
|
||||
import kotlinx.android.synthetic.main.comment.view.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
abstract class PostUtils {
|
||||
companion object {
|
||||
fun toggleCommentInput(
|
||||
holder : PostViewHolder
|
||||
) {
|
||||
//Toggle comment button
|
||||
holder.commenter.setOnClickListener {
|
||||
when(holder.commentIn.visibility) {
|
||||
View.VISIBLE -> {
|
||||
holder.commentIn.visibility = View.GONE
|
||||
setImageFromDrawable(holder.postView, holder.commenter, R.drawable.ic_comment_empty)
|
||||
}
|
||||
View.GONE -> {
|
||||
holder.commentIn.visibility = View.VISIBLE
|
||||
setImageFromDrawable(holder.postView, holder.commenter, R.drawable.ic_comment_blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reblogPost(
|
||||
holder : PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
post : Status
|
||||
) {
|
||||
//Call the api function
|
||||
api.reblogStatus(credential, post.id!!).enqueue(object : Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("REBLOG ERROR", t.toString())
|
||||
holder.reblogger.isChecked = false
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
|
||||
//Update shown share count
|
||||
holder.nshares.text = resp.getNShares(holder.context)
|
||||
holder.reblogger.isChecked = resp.reblogged!!
|
||||
} else {
|
||||
Log.e("RESPONSE_CODE", response.code().toString())
|
||||
holder.reblogger.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun undoReblogPost(
|
||||
holder : PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
post : Status
|
||||
) {
|
||||
//Call the api function
|
||||
api.undoReblogStatus(credential, post.id!!).enqueue(object : Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("REBLOG ERROR", t.toString())
|
||||
holder.reblogger.isChecked = true
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
|
||||
//Update shown share count
|
||||
holder.nshares.text = resp.getNShares(holder.context)
|
||||
holder.reblogger.isChecked = resp.reblogged!!
|
||||
} else {
|
||||
Log.e("RESPONSE_CODE", response.code().toString())
|
||||
holder.reblogger.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun likePostCall(
|
||||
holder : PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
post : Status
|
||||
) {
|
||||
//Call the api function
|
||||
api.likePost(credential, post.id!!).enqueue(object : Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("LIKE ERROR", t.toString())
|
||||
holder.liker.isChecked = false
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
|
||||
//Update shown like count and internal like toggle
|
||||
holder.nlikes.text = resp.getNLikes(holder.context)
|
||||
holder.liker.isChecked = resp.favourited ?: false
|
||||
} else {
|
||||
Log.e("RESPONSE_CODE", response.code().toString())
|
||||
holder.liker.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun unLikePostCall(
|
||||
holder : PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
post : Status
|
||||
) {
|
||||
//Call the api function
|
||||
api.unlikePost(credential, post.id!!).enqueue(object : Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("UNLIKE ERROR", t.toString())
|
||||
holder.liker.isChecked = true
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
|
||||
//Update shown like count and internal like toggle
|
||||
holder.nlikes.text = resp.getNLikes(holder.context)
|
||||
holder.liker.isChecked = resp.favourited ?: false
|
||||
} else {
|
||||
Log.e("RESPONSE_CODE", response.code().toString())
|
||||
holder.liker.isChecked = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun postComment(
|
||||
holder : PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
post : Status
|
||||
) {
|
||||
val textIn = holder.comment.text
|
||||
val nonNullText = textIn.toString()
|
||||
api.postStatus(credential, nonNullText, post.id).enqueue(object :
|
||||
Callback<Status> {
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
Log.e("COMMENT ERROR", t.toString())
|
||||
Toast.makeText(holder.context, holder.context.getString(R.string.comment_error),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
//Check that the received response code is valid
|
||||
if(response.code() == 200) {
|
||||
val resp = response.body()!!
|
||||
holder.commentIn.visibility = View.GONE
|
||||
|
||||
//Add the comment to the comment section
|
||||
addComment(holder.context, holder.commentCont, resp.account!!.username!!,
|
||||
resp.content!!
|
||||
)
|
||||
|
||||
Toast.makeText(holder.context,
|
||||
holder.context.getString(R.string.comment_posted).format(textIn),
|
||||
Toast.LENGTH_SHORT).show()
|
||||
Log.e("COMMENT SUCCESS", "posted: $textIn")
|
||||
} else {
|
||||
Log.e("ERROR_CODE", response.code().toString())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun addComment(context: android.content.Context, commentContainer: LinearLayout, commentUsername: String, commentContent: String) {
|
||||
|
||||
val view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.comment, commentContainer, true)
|
||||
|
||||
view.user.text = commentUsername
|
||||
view.commentText.text = commentContent
|
||||
}
|
||||
|
||||
fun retrieveComments(
|
||||
holder : PostViewHolder,
|
||||
api: PixelfedAPI,
|
||||
credential: String,
|
||||
post : Status
|
||||
) {
|
||||
api.statusComments(post.id!!, credential).enqueue(object :
|
||||
Callback<Context> {
|
||||
override fun onFailure(call: Call<Context>, t: Throwable) {
|
||||
Log.e("COMMENT FETCH ERROR", t.toString())
|
||||
}
|
||||
|
||||
override fun onResponse(
|
||||
call: Call<Context>,
|
||||
response: Response<Context>
|
||||
) {
|
||||
if(response.code() == 200) {
|
||||
val statuses = response.body()!!.descendants
|
||||
|
||||
holder.commentCont.removeAllViews()
|
||||
|
||||
//Create the new views for each comment
|
||||
for (status in statuses) {
|
||||
addComment(holder.context, holder.commentCont, status.account!!.username!!,
|
||||
status.content!!
|
||||
)
|
||||
}
|
||||
holder.commentCont.visibility = View.VISIBLE
|
||||
} else {
|
||||
Log.e("COMMENT ERROR", "${response.code()} with body ${response.errorBody()}")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun censorColorMatrix(): ColorMatrix {
|
||||
val array: FloatArray = floatArrayOf( 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f, 0f )
|
||||
|
@ -20,6 +20,7 @@
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/postFragmentSingle"
|
||||
android:layout_width="match_parent"
|
||||
@ -28,8 +29,9 @@
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" >
|
||||
</androidx.fragment.app.FragmentContainerView>
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:context=".fragments.PostFragment"
|
||||
tools:visibility="visible"/>
|
||||
</ScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.ImageFragment">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<ImageView
|
||||
@ -13,5 +13,4 @@
|
||||
android:adjustViewBounds="true"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
|
||||
</FrameLayout>
|
@ -6,6 +6,17 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="wrap_content"
|
||||
@ -16,23 +27,36 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layoutManager="LinearLayoutManager" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout
|
||||
android:id="@+id/motionLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="visible"
|
||||
app:layoutDescription="@xml/error_layout_xml_error_scene"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/followButton"
|
||||
tools:visibility="visible">
|
||||
|
||||
<include
|
||||
android:id="@id/errorLayout"
|
||||
layout="@layout/error_layout"
|
||||
tools:layout_editor_absoluteX="50dp" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/errorLayout"
|
||||
app:layoutManager="LinearLayoutManager"
|
||||
tools:listitem="@layout/post_fragment"/>
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include layout="@layout/error_layout"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
45
app/src/main/res/layout/load_state_footer_view_item.xml
Normal file
45
app/src/main/res/layout/load_state_footer_view_item.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2018 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.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
<TextView
|
||||
android:id="@+id/error_msg"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="24sp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textAlignment="center"
|
||||
tools:text="Timeout"/>
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"/>
|
||||
<Button
|
||||
android:id="@+id/retry_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/retry"/>
|
||||
</LinearLayout>
|
File diff suppressed because one or more lines are too long
@ -37,10 +37,10 @@ class PostUnitTest {
|
||||
@Test
|
||||
fun getUsernameReturnsACorrectName() = Assert.assertEquals(status.account!!.display_name, status.account!!.getDisplayName())
|
||||
|
||||
@Test
|
||||
/*@Test
|
||||
fun getUsernameReturnsOtherNameIfUsernameIsNull() {
|
||||
val emptyDescStatus = status.copy(account = status.account!!.copy(username = ""))
|
||||
Assert.assertEquals(status.account!!.display_name, emptyDescStatus.account!!.getDisplayName())
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.4.10'
|
||||
ext.kotlin_version = '1.4.20'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
@ -22,3 +22,4 @@ kotlin.code.style=official
|
||||
org.gradle.daemon=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
kapt.incremental.apt=true
|
||||
|
Loading…
x
Reference in New Issue
Block a user