Merge pull request #85 from H-PixelDroid/scroll_load

Load images in advance, infinite scrolling
This commit is contained in:
Sanimys 2020-04-03 10:32:19 +02:00 committed by GitHub
commit 5372e7e9ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 444 additions and 315 deletions

View File

@ -94,6 +94,8 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
implementation 'androidx.paging:paging-runtime-ktx:2.1.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
}

View File

@ -54,6 +54,8 @@ class AppDatabaseTest {
@Test
fun testUtilsLRU() {
for(i in 1..db!!.MAX_NUMBER_OF_POSTS) {
//sleep a bit to not have the weird concurrency bugs?
Thread.sleep(10)
DatabaseUtils.insertAllPosts(db!!, PostEntity(i, i.toString(), date= Calendar.getInstance().time))
}

File diff suppressed because one or more lines are too long

View File

@ -44,7 +44,7 @@ interface PixelfedAPI {
@Query("max_id") max_id: String? = null,
@Query("since_id") since_id: String? = null,
@Query("min_id") min_id: String? = null,
@Query("limit") limit: Int? = null
@Query("limit") limit: String? = null
): Call<List<Status>>
@ -55,7 +55,7 @@ interface PixelfedAPI {
@Query("max_id") max_id: String? = null,
@Query("since_id") since_id: String? = null,
@Query("min_id") min_id: String? = null,
@Query("limit") limit: Int? = null,
@Query("limit") limit: String? = null,
@Query("local") local: Boolean? = null
): Call<List<Status>>

View File

@ -8,6 +8,8 @@ import android.view.ViewGroup
import com.h.pixeldroid.R
import com.h.pixeldroid.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.ImageConverter
import kotlinx.android.synthetic.main.post_fragment.view.*
class PostFragment : Fragment() {
@ -18,7 +20,18 @@ class PostFragment : Fragment() {
): View? {
val status = arguments?.getSerializable(POST_TAG) as Status?
val root = inflater.inflate(R.layout.post_fragment, container, false)
status?.setupPost(this, root)
status?.setupPost(root)
//Setup post and profile images
ImageConverter.setImageViewFromURL(
this,
status?.getPostUrl(),
root.postPicture
)
ImageConverter.setImageViewFromURL(
this,
status?.getProfilePicUrl(),
root.profilePic
)
return root
}

View File

@ -9,21 +9,31 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.DataSource
import androidx.paging.ItemKeyedDataSource
import androidx.paging.PagedList
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.bumptech.glide.ListPreloader.PreloadModelProvider
import com.h.pixeldroid.BuildConfig
import com.h.pixeldroid.R
import com.h.pixeldroid.api.PixelfedAPI
import com.h.pixeldroid.objects.FeedContent
import kotlinx.android.synthetic.main.fragment_feed.*
import kotlinx.android.synthetic.main.fragment_feed.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
open class FeedFragment<T: FeedContent, VH: RecyclerView.ViewHolder?>: Fragment() {
var content : List<T> = ArrayList()
lateinit var content: LiveData<PagedList<T>>
lateinit var factory: FeedDataSourceFactory
protected var accessToken: String? = null
protected lateinit var pixelfedAPI: PixelfedAPI
@ -33,34 +43,13 @@ open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
protected lateinit var adapter : FeedsRecyclerViewAdapter<T, VH>
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
protected fun doRequest(call: Call<List<T>>){
call.enqueue(object : Callback<List<T>> {
override fun onResponse(call: Call<List<T>>, response: Response<List<T>>) {
if (response.code() == 200) {
val notifications = response.body()!! as ArrayList<T>
setContent(notifications)
} else{
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
}
swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE
}
override fun onFailure(call: Call<List<T>>, t: Throwable) {
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
Log.e("FeedFragment", t.toString())
}
})
}
protected fun setContent(content : ArrayList<T>) {
this.content = content
adapter.initializeWith(content)
}
protected fun onCreateView(inflater: LayoutInflater, container: ViewGroup?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_feed, container, false)
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshLayout)
list = swipeRefreshLayout.list
// Set the adapter
@ -69,7 +58,6 @@ open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -80,19 +68,88 @@ open class FeedFragment<T, VH: RecyclerView.ViewHolder?>: Fragment() {
pixelfedAPI = PixelfedAPI.create("${preferences.getString("domain", "")}")
accessToken = preferences.getString("accessToken", "")
swipeRefreshLayout.setOnRefreshListener {
//by invalidating data, loadInitial will be called again
factory.liveData.value!!.invalidate()
}
}
inner class FeedDataSource(private val makeInitialCall: (Int) -> Call<List<T>>,
private val makeAfterCall: (Int, String) -> Call<List<T>>
): ItemKeyedDataSource<String, T>() {
//We use the id as the key
override fun getKey(item: T): String {
return item.id
}
//This is called to initialize the list, so we want some of the latest statuses
override fun loadInitial(
params: LoadInitialParams<String>,
callback: LoadInitialCallback<T>
) {
enqueueCall(makeInitialCall(params.requestedLoadSize), callback)
}
//This is called to when we get to the bottom of the loaded content, so we want statuses
//older than the given key (params.key)
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<T>) {
enqueueCall(makeAfterCall(params.requestedLoadSize, params.key), callback)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<T>) {
//do nothing here, it is expected to pull to refresh to load newer notifications
}
private fun enqueueCall(call: Call<List<T>>, callback: LoadCallback<T>){
call.enqueue(object : Callback<List<T>> {
override fun onResponse(call: Call<List<T>>, response: Response<List<T>>) {
if (response.code() == 200) {
val notifications = response.body()!! as ArrayList<T>
callback.onResult(notifications as List<T>)
} else{
Toast.makeText(context,"Something went wrong while loading", Toast.LENGTH_SHORT).show()
}
swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE
}
override fun onFailure(call: Call<List<T>>, t: Throwable) {
Toast.makeText(context,"Could not get feed", Toast.LENGTH_SHORT).show()
Log.e("FeedFragment", t.toString())
}
})
}
}
inner class FeedDataSourceFactory(
private val makeInitialCall: (Int) -> Call<List<T>>,
private val makeAfterCall: (Int, String) -> Call<List<T>>
): DataSource.Factory<String, T>() {
lateinit var liveData: MutableLiveData<FeedDataSource>
override fun create(): DataSource<String, T> {
val dataSource = FeedDataSource(::makeInitialCall.get(), ::makeAfterCall.get())
liveData = MutableLiveData()
liveData.postValue(dataSource)
return dataSource
}
}
}
abstract class FeedsRecyclerViewAdapter<T, VH : RecyclerView.ViewHolder?>: RecyclerView.Adapter<VH>() {
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
}
protected val feedContent: ArrayList<T> = arrayListOf()
protected lateinit var context: Context
override fun getItemCount(): Int = feedContent.size
open fun initializeWith(content: List<T>){
feedContent.clear()
feedContent.addAll(content)
notifyDataSetChanged()
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem == newItem
}
}
}
), PreloadModelProvider<T> {
protected lateinit var context: Context
}

View File

@ -1,34 +1,143 @@
package com.h.pixeldroid.fragments.feeds
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.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.util.ViewPreloadSizeProvider
import com.h.pixeldroid.R
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.ImageConverter
import kotlinx.android.synthetic.main.fragment_home.*
import retrofit2.Call
class HomeFragment : FeedFragment<Status, HomeRecyclerViewAdapter.ViewHolder>() {
class HomeFragment : FeedFragment<Status, HomeFragment.HomeRecyclerViewAdapter.ViewHolder>() {
lateinit var picRequest: RequestBuilder<Drawable>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container)
val view = super.onCreateView(inflater, container, savedInstanceState)
content = makeContent()
//RequestBuilder that is re-used for every image
picRequest = Glide.with(this)
.asDrawable().fitCenter()
.placeholder(ColorDrawable(Color.GRAY))
adapter = HomeRecyclerViewAdapter()
list.adapter = adapter
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
})
//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, sizeProvider, 4
)
list.addOnScrollListener(preloader)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
swipeRefreshLayout.setOnRefreshListener {
val call = pixelfedAPI.timelineHome("Bearer $accessToken")
doRequest(call)
private fun makeContent(): LiveData<PagedList<Status>> {
fun makeInitialCall(requestedLoadSize: Int): Call<List<Status>> {
return pixelfedAPI
.timelineHome("Bearer $accessToken", limit="$requestedLoadSize")
}
fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Status>> {
return pixelfedAPI
.timelineHome("Bearer $accessToken", max_id=key,
limit="$requestedLoadSize")
}
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory = FeedDataSourceFactory(::makeInitialCall, ::makeAfterCall)
return LivePagedListBuilder(factory, config).build()
}
/**
* [RecyclerView.Adapter] that can display a list of Statuses
*/
inner class HomeRecyclerViewAdapter: FeedsRecyclerViewAdapter<Status, HomeRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.post_fragment, parent, false)
context = view.context
return ViewHolder(view)
}
/**
* Binds the different elements of the Post Model to the view holder
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = getItem(position) ?: return
val metrics = context.resources.displayMetrics
//Limit the height of the different images
holder.profilePic?.maxHeight = metrics.heightPixels
holder.postPic.maxHeight = metrics.heightPixels
//Set the two images
ImageConverter.setRoundImageFromURL(
holder.postView,
post.getProfilePicUrl(),
holder.profilePic!!
)
picRequest.load(post.getPostUrl()).into(holder.postPic)
//Set the image back to a placeholder if the original is too big
if(holder.postPic.height > metrics.heightPixels) {
ImageConverter.setDefaultImage(holder.postView, holder.postPic)
}
//Set the the text views
post.setupPost(holder.postView)
}
/**
* Represents the posts that will be contained within the feed
*/
inner class ViewHolder(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 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)
}
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())
}
val call = pixelfedAPI.timelineHome("Bearer $accessToken")
doRequest(call)
}
}

View File

@ -1,79 +0,0 @@
package com.h.pixeldroid.fragments.feeds
import android.graphics.Typeface
import android.util.DisplayMetrics
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.h.pixeldroid.R
import com.h.pixeldroid.objects.Status
import com.h.pixeldroid.utils.ImageConverter.Companion.setDefaultImage
import com.h.pixeldroid.utils.ImageConverter.Companion.setImageViewFromURL
import com.h.pixeldroid.utils.ImageConverter.Companion.setRoundImageFromURL
/**
* [RecyclerView.Adapter] that can display a list of Posts
*/
class HomeRecyclerViewAdapter() : FeedsRecyclerViewAdapter<Status, HomeRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.post_fragment, parent, false)
context = view.context
return ViewHolder(view)
}
/**
* Binds the different elements of the Post Model to the view holder
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = feedContent[position]
val metrics = DisplayMetrics()
//Limit the height of the different images
holder.profilePic?.maxHeight = metrics.heightPixels
holder.postPic.maxHeight = metrics.heightPixels
//Set the two images
setRoundImageFromURL(holder.postView, post.getProfilePicUrl(), holder.profilePic!!)
setImageViewFromURL(holder.postView, post.getPostUrl(), holder.postPic)
//Set the image back to a placeholder if the original is too big
if(holder.postPic.height > metrics.heightPixels) {
setDefaultImage(holder.postView, holder.postPic)
}
//Set the the text views
holder.username.text = post.getUsername()
holder.username.setTypeface(null, Typeface.BOLD)
holder.usernameDesc.text = post.getUsername()
holder.usernameDesc.setTypeface(null, Typeface.BOLD)
holder.description.text = post.getDescription()
holder.nlikes.text = post.getNLikes()
holder.nlikes.setTypeface(null, Typeface.BOLD)
holder.nshares.text = post.getNShares()
holder.nshares.setTypeface(null, Typeface.BOLD)
}
override fun getItemCount(): Int = feedContent.size
/**
* Represents the posts that will be contained within the feed
*/
inner class ViewHolder(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 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)
}
}

View File

@ -1,44 +1,199 @@
package com.h.pixeldroid.fragments.feeds
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.net.Uri
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 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.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.R
import com.h.pixeldroid.objects.Notification
import com.h.pixeldroid.objects.Status
import kotlinx.android.synthetic.main.fragment_feed.*
import kotlinx.android.synthetic.main.fragment_notifications.view.*
import retrofit2.Call
/**
* A fragment representing a list of Items.
*/
class NotificationsFragment : FeedFragment<Notification, NotificationsRecyclerViewAdapter.ViewHolder>() {
class NotificationsFragment : FeedFragment<Notification, NotificationsFragment.NotificationsRecyclerViewAdapter.ViewHolder>() {
lateinit var profilePicRequest: RequestBuilder<Drawable>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = super.onCreateView(inflater, container)
val view = super.onCreateView(inflater, container, savedInstanceState)
content = makeContent()
//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
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
})
//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, sizeProvider, 4
)
list.addOnScrollListener(preloader)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
swipeRefreshLayout.setOnRefreshListener {
val call = pixelfedAPI.notifications("Bearer $accessToken", min_id="1")
doRequest(call)
private fun makeContent(): LiveData<PagedList<Notification>> {
fun makeInitialCall(requestedLoadSize: Int): Call<List<Notification>> {
return pixelfedAPI
.notifications("Bearer $accessToken", min_id="1", limit="$requestedLoadSize")
}
val call = pixelfedAPI.notifications("Bearer $accessToken", min_id="1")
doRequest(call)
fun makeAfterCall(requestedLoadSize: Int, key: String): Call<List<Notification>> {
return pixelfedAPI
.notifications("Bearer $accessToken", max_id=key, limit="$requestedLoadSize")
}
val config: PagedList.Config = PagedList.Config.Builder().setPageSize(10).build()
factory = FeedDataSourceFactory(::makeInitialCall, ::makeAfterCall)
return LivePagedListBuilder(factory, config).build()
}
/**
* [RecyclerView.Adapter] that can display a [Notification]
*/
inner class NotificationsRecyclerViewAdapter: FeedsRecyclerViewAdapter<Notification, NotificationsRecyclerViewAdapter.ViewHolder>() {
private val mOnClickListener: View.OnClickListener
init {
mOnClickListener = View.OnClickListener { v ->
val notification = v.tag as Notification
openActivity(notification)
}
}
private fun openActivity(notification: Notification){
val intent: Intent
when (notification.type){
Notification.NotificationType.mention, Notification.NotificationType.favourite-> {
intent = Intent(context, PostActivity::class.java)
intent.putExtra(Status.POST_TAG, notification.status)
}
Notification.NotificationType.reblog-> {
Toast.makeText(context,"Can't see shares yet, sorry!", Toast.LENGTH_SHORT).show()
return
}
Notification.NotificationType.follow -> {
val url = notification.status?.url ?: notification.account.url
intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
}
}
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)
holder.postDescription.text = notification.status?.content ?: ""
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_share)
}
Notification.NotificationType.favourite -> {
setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_heart)
}
}
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 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)
}
}
}

View File

@ -1,127 +0,0 @@
package com.h.pixeldroid.fragments.feeds
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.h.pixeldroid.PostActivity
import com.h.pixeldroid.R
import com.h.pixeldroid.objects.Status.Companion.POST_TAG
import com.h.pixeldroid.objects.Notification
import kotlinx.android.synthetic.main.fragment_notifications.view.*
/**
* [RecyclerView.Adapter] that can display a [Notification]
*/
class NotificationsRecyclerViewAdapter: FeedsRecyclerViewAdapter<Notification, NotificationsRecyclerViewAdapter.ViewHolder>() {
private val mOnClickListener: View.OnClickListener
init {
mOnClickListener = View.OnClickListener { v ->
val notification = v.tag as Notification
openActivity(notification)
}
}
private fun openActivity(notification: Notification){
val intent: Intent
when (notification.type){
Notification.NotificationType.mention, Notification.NotificationType.favourite-> {
intent = Intent(context, PostActivity::class.java)
intent.putExtra(POST_TAG, notification.status)
}
Notification.NotificationType.reblog-> {
Toast.makeText(context,"Can't see shares yet, sorry!",Toast.LENGTH_SHORT).show()
return
}
Notification.NotificationType.follow -> {
val url = notification.status?.url ?: notification.account.url
intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
}
}
context.startActivity(intent)
}
fun addNotifications(newNotifications: List<Notification>){
val oldSize = feedContent.size
feedContent.addAll(newNotifications)
notifyItemRangeInserted(oldSize, newNotifications.size)
}
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 = feedContent[position]
Glide.with(holder.mView).load(notification.account.avatar_static).apply(RequestOptions().circleCrop())
.placeholder(R.drawable.ic_default_user).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)
holder.postDescription.text = notification.status?.content ?: ""
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_share)
}
Notification.NotificationType.favourite -> {
setNotificationTypeTextView(context, R.string.liked_notification, R.drawable.ic_heart)
}
}
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 postDescription: TextView = mView.notification_post_description
val avatar: ImageView = mView.notification_avatar
val photoThumbnail: ImageView = mView.notification_photo_thumbnail
}
}

View File

@ -0,0 +1,14 @@
package com.h.pixeldroid.objects
abstract class FeedContent {
abstract val id: String
override fun equals(other: Any?): Boolean {
return super.equals(other)
}
override fun hashCode(): Int {
return id.hashCode()
}
}

View File

@ -4,15 +4,15 @@ package com.h.pixeldroid.objects
Represents a notification of an event relevant to the user.
https://docs.joinmastodon.org/entities/notification/
*/
data class Notification (
data class Notification(
//Required attributes
val id: String,
override val id: String,
val type: NotificationType,
val created_at: String, //ISO 8601 Datetime
val account: Account,
//Optional attributes
val status: Status? = null
) {
): FeedContent() {
enum class NotificationType {
follow, mention, reblog, favourite
}

View File

@ -6,6 +6,7 @@ import android.widget.TextView
import androidx.fragment.app.Fragment
import com.h.pixeldroid.R
import com.h.pixeldroid.utils.ImageConverter
import kotlinx.android.synthetic.main.post_fragment.view.*
import java.io.Serializable
/*
@ -14,7 +15,7 @@ https://docs.joinmastodon.org/entities/status/
*/
data class Status(
//Base attributes
val id: String,
override val id: String,
val uri: String,
val created_at: String, //ISO 8601 Datetime (maybe can use a date type)
val account: Account,
@ -47,7 +48,7 @@ data class Status(
val muted: Boolean,
val bookmarked: Boolean,
val pinned: Boolean
) : Serializable
) : Serializable, FeedContent()
{
companion object {
@ -67,15 +68,12 @@ data class Status(
}
fun getUsername() : CharSequence {
var name = account?.username
var name = account?.display_name
if (name.isNullOrEmpty()) {
name = account?.display_name
name = account?.username
}
return name!!
}
fun getUsernameDescription() : CharSequence {
return account?.display_name ?: ""
}
fun getNLikes() : CharSequence {
val nLikes : Int = favourites_count ?: 0
@ -87,38 +85,22 @@ data class Status(
return "$nShares Shares"
}
fun setupPost(fragment: Fragment, rootView : View) {
fun setupPost(rootView : View) {
//Setup username as a button that opens the profile
val username = rootView.findViewById<TextView>(R.id.username)
username.text = this.getUsername()
username.setTypeface(null, Typeface.BOLD)
rootView.username.text = this.getUsername()
rootView.username.setTypeface(null, Typeface.BOLD)
val usernameDesc = rootView.findViewById<TextView>(R.id.usernameDesc)
usernameDesc.text = this.getUsernameDescription()
usernameDesc.setTypeface(null, Typeface.BOLD)
rootView.usernameDesc.text = this.getUsername()
rootView.usernameDesc.setTypeface(null, Typeface.BOLD)
val description = rootView.findViewById<TextView>(R.id.description)
description.text = this.getDescription()
rootView.description.text = this.getDescription()
val nlikes = rootView.findViewById<TextView>(R.id.nlikes)
nlikes.text = this.getNLikes()
nlikes.setTypeface(null, Typeface.BOLD)
rootView.nlikes.text = this.getNLikes()
rootView.nlikes.setTypeface(null, Typeface.BOLD)
val nshares = rootView.findViewById<TextView>(R.id.nshares)
nshares.text = this.getNShares()
nshares.setTypeface(null, Typeface.BOLD)
rootView.nshares.text = this.getNShares()
rootView.nshares.setTypeface(null, Typeface.BOLD)
//Setup post and profile images
ImageConverter.setImageViewFromURL(
fragment,
getPostUrl(),
rootView.findViewById(R.id.postPicture)
)
ImageConverter.setImageViewFromURL(
fragment,
getProfilePicUrl(),
rootView.findViewById(R.id.profilePic)
)
}
enum class Visibility : Serializable {
public, unlisted, private, direct

View File

@ -52,6 +52,7 @@ tools:context=".fragments.PostFragment">
<ImageView
android:id="@+id/postPicture"
android:adjustViewBounds="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
@ -73,7 +74,7 @@ tools:context=".fragments.PostFragment">
android:id="@+id/nlikes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginStart="30dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:layout_weight="50"
@ -84,10 +85,10 @@ tools:context=".fragments.PostFragment">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginRight="30dp"
android:layout_marginEnd="30dp"
android:layout_marginBottom="10dp"
android:layout_weight="50"
android:gravity="right"
android:gravity="end"
tools:text="TextView" />
</LinearLayout>
@ -95,7 +96,7 @@ tools:context=".fragments.PostFragment">
android:id="@+id/usernameDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
tools:text="TextView" />

View File

@ -45,7 +45,7 @@ class PostUnitTest {
fun getDescriptionReturnsACorrectDesc() = Assert.assertEquals(status.content, status.getDescription())
@Test
fun getUsernameReturnsACorrectName() = Assert.assertEquals(status.account.username, status.getUsername())
fun getUsernameReturnsACorrectName() = Assert.assertEquals(status.account.display_name, status.getUsername())
@Test
fun getUsernameReturnsOtherNameIfUsernameIsNull() {