Add basic lists support (#501)

main
Ivan Kupalov 7 years ago committed by Konrad Pozniak
parent 9262edad18
commit 3aefe4bda5
  1. 2
      app/src/main/AndroidManifest.xml
  2. 199
      app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt
  3. 4
      app/src/main/java/com/keylesspalace/tusky/MainActivity.java
  4. 63
      app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt
  5. 7
      app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt
  6. 37
      app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
  7. 10
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java
  8. 37
      app/src/main/res/layout/activity_lists.xml
  9. 27
      app/src/main/res/layout/activity_modal_timeline.xml
  10. 21
      app/src/main/res/layout/item_list.xml
  11. 1
      app/src/main/res/layout/toolbar_basic.xml
  12. 20
      app/src/main/res/layout/toolbar_shadow_shim.xml
  13. 3
      app/src/main/res/values/strings.xml

@ -95,6 +95,8 @@
</intent-filter>
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />
</activity>
<activity android:name=".ListsActivity" />
<activity android:name=".ModalTimelineActivity" />
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />

@ -0,0 +1,199 @@
package com.keylesspalace.tusky
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.widget.TextViewCompat
import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable
import com.varunest.sparkbutton.helpers.Utils
import retrofit2.Call
import retrofit2.Response
import java.lang.ref.WeakReference
/**
* Created by charlag on 1/4/18.
*/
interface ListsView {
fun update(state: State)
fun openTimeline(listId: String)
}
data class State(val lists: List<MastoList>, val isLoading: Boolean)
class ListsViewModel(private val api: MastodonApi) {
private var _view: WeakReference<ListsView>? = null
private val view: ListsView? get() = _view?.get()
private var state = State(listOf(), false)
fun attach(view: ListsView) {
this._view = WeakReference(view)
updateView()
loadIfNeeded()
}
fun detach() {
this._view = null
}
fun didSelectItem(id: String) {
view?.openTimeline(id)
}
private fun loadIfNeeded() {
if (state.isLoading || !state.lists.isEmpty()) return
updateState(state.copy(isLoading = false))
api.getLists().enqueue(object : retrofit2.Callback<List<MastoList>> {
override fun onResponse(call: Call<List<MastoList>>, response: Response<List<MastoList>>) {
updateState(state.copy(lists = response.body() ?: listOf(), isLoading = false))
}
override fun onFailure(call: Call<List<MastoList>>, t: Throwable?) {
updateState(state.copy(isLoading = false))
}
})
}
private fun updateState(state: State) {
this.state = state
view?.update(state)
}
private fun updateView() {
view?.update(state)
}
}
class ListsActivity : BaseActivity(), ListsView {
companion object {
@JvmStatic
fun newIntent(context: Context): Intent {
return Intent(context, ListsActivity::class.java)
}
}
private lateinit var recyclerView: RecyclerView
private lateinit var progressBar: ProgressBar
private lateinit var viewModel: ListsViewModel
private val adapter = ListsAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lists)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
recyclerView = findViewById(R.id.lists_recycler)
progressBar = findViewById(R.id.progress_bar)
setSupportActionBar(toolbar)
val bar = supportActionBar
if (bar != null) {
bar.title = getString(R.string.title_lists)
bar.setDisplayHomeAsUpEnabled(true)
bar.setDisplayShowHomeEnabled(true)
}
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
viewModel = lastNonConfigurationInstance as? ListsViewModel ?: ListsViewModel(mastodonApi)
viewModel.attach(this)
}
override fun onDestroy() {
viewModel.detach()
super.onDestroy()
}
override fun onRetainCustomNonConfigurationInstance(): Any {
return viewModel
}
override fun update(state: State) {
adapter.update(state.lists)
progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
}
override fun openTimeline(listId: String) {
startActivity(
ModalTimelineActivity.newIntent(this, TimelineFragment.Kind.LIST, listId))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return false
}
private inner class ListsAdapter : RecyclerView.Adapter<ListsAdapter.ListViewHolder>() {
private val items = mutableListOf<MastoList>()
fun update(list: List<MastoList>) {
this.items.clear()
this.items.addAll(list)
notifyDataSetChanged()
}
override fun getItemCount(): Int = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
.let(this::ListViewHolder)
.apply {
val context = nameTextView.context
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list)
val size = Utils.dpToPx(context, 20)
ThemeUtils.setDrawableTint(context, icon, android.R.attr.textColorTertiary)
icon.setBounds(0, 0, size, size)
nameTextView.compoundDrawablePadding = Utils.dpToPx(context, 8)
TextViewCompat.setCompoundDrawablesRelative(
nameTextView, icon, null, null, null)
}
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
holder.nameTextView.text = items[position].title
}
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener {
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
init {
view.setOnClickListener(this)
}
override fun onClick(v: View?) {
viewModel.didSelectItem(items[adapterPosition].id)
}
}
}
}

@ -77,6 +77,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
private static final long DRAWER_ITEM_LOG_OUT = 7;
private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 8;
private static final long DRAWER_ITEM_SAVED_TOOT = 9;
private static final long DRAWER_ITEM_LISTS = 10;
private static int COMPOSE_RESULT = 1;
@ -311,6 +312,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
List<IDrawerItem> listItem = new ArrayList<>();
listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_EDIT_PROFILE).withName(getString(R.string.action_edit_profile)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_person));
listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_FAVOURITES).withName(getString(R.string.action_view_favourites)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_star));
listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_LISTS).withName(R.string.action_lists).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_list));
listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_MUTED_USERS).withName(getString(R.string.action_view_mutes)).withSelectable(false).withIcon(muteDrawable));
listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_BLOCKED_USERS).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block));
listItem.add(new PrimaryDrawerItem().withIdentifier(DRAWER_ITEM_SEARCH).withName(getString(R.string.action_search)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_search));
@ -366,6 +368,8 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
} else if (drawerItemIdentifier == DRAWER_ITEM_SAVED_TOOT) {
Intent intent = new Intent(MainActivity.this, SavedTootActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
startActivity(ListsActivity.newIntent(this));
}
}

@ -0,0 +1,63 @@
package com.keylesspalace.tusky
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.design.widget.FloatingActionButton
import android.support.v7.widget.Toolbar
import android.view.MenuItem
import android.widget.FrameLayout
import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
class ModalTimelineActivity : BaseActivity(), ActionButtonActivity {
companion object {
private const val ARG_KIND = "kind"
private const val ARG_ARG = "arg"
@JvmStatic fun newIntent(context: Context, kind: TimelineFragment.Kind,
argument: String?): Intent {
val intent = Intent(context, ModalTimelineActivity::class.java)
intent.putExtra(ARG_KIND, kind)
intent.putExtra(ARG_ARG, argument)
return intent
}
}
lateinit var contentFrame: FrameLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_modal_timeline)
contentFrame = findViewById(R.id.content_frame)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val bar = supportActionBar
if (bar != null) {
bar.title = getString(R.string.title_list_timeline)
bar.setDisplayHomeAsUpEnabled(true)
bar.setDisplayShowHomeEnabled(true)
}
if (supportFragmentManager.findFragmentById(R.id.content_frame) == null) {
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind ?:
TimelineFragment.Kind.HOME
val argument = intent?.getStringExtra(ARG_ARG)
supportFragmentManager.beginTransaction()
.replace(R.id.content_frame, TimelineFragment.newInstance(kind, argument))
.commit()
}
}
override fun getActionButton(): FloatingActionButton? = null
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return false
}
}

@ -0,0 +1,7 @@
package com.keylesspalace.tusky.entity
/**
* Created by charlag on 1/4/18.
*/
data class MastoList(val id: String, val title: String)

@ -80,7 +80,8 @@ public class TimelineFragment extends SFragment implements
PUBLIC_FEDERATED,
TAG,
USER,
FAVOURITES
FAVOURITES,
LIST
}
private enum FetchEnd {
@ -158,7 +159,7 @@ public class TimelineFragment extends SFragment implements
Bundle savedInstanceState) {
Bundle arguments = getArguments();
kind = Kind.valueOf(arguments.getString(KIND_ARG));
if (kind == Kind.TAG || kind == Kind.USER) {
if (kind == Kind.TAG || kind == Kind.USER || kind == Kind.LIST) {
hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG);
}
@ -209,19 +210,23 @@ public class TimelineFragment extends SFragment implements
if (jumpToTopAllowed()) {
TabLayout layout = getActivity().findViewById(R.id.tab_layout);
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
if (layout != null) {
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
jumpToTop();
}
};
layout.addOnTabSelectedListener(onTabSelectedListener);
@Override
public void onTabReselected(TabLayout.Tab tab) {
jumpToTop();
}
};
layout.addOnTabSelectedListener(onTabSelectedListener);
}
}
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
@ -273,7 +278,9 @@ public class TimelineFragment extends SFragment implements
public void onDestroyView() {
if (jumpToTopAllowed()) {
TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout);
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
if (tabLayout != null) {
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
}
}
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(timelineReceiver);
super.onDestroyView();
@ -532,6 +539,8 @@ public class TimelineFragment extends SFragment implements
return api.accountStatuses(tagOrId, fromId, uptoId, LOAD_AT_ONCE, null);
case FAVOURITES:
return api.favourites(fromId, uptoId, LOAD_AT_ONCE);
case LIST:
return api.listTimeline(tagOrId, fromId, uptoId, LOAD_AT_ONCE);
}
}

@ -22,6 +22,7 @@ import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.AppCredentials;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.MastoList;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Profile;
import com.keylesspalace.tusky.entity.Relationship;
@ -67,6 +68,12 @@ public interface MastodonApi {
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/timelines/list/{listId}")
Call<List<Status>> listTimeline(
@Path("listId") String listId,
@Query("max_id") String maxId,
@Query("since_id") String sinceId,
@Query("limit") Integer limit);
@GET("api/v1/notifications")
Call<List<Notification>> notifications(
@ -236,4 +243,7 @@ public interface MastodonApi {
Call<Card> statusCard(
@Path("id") String statusId
);
@GET("/api/v1/lists")
Call<List<MastoList>> getLists();
}

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/toolbar_basic" />
<include
layout="@layout/toolbar_shadow_shim"
android:layout_width="0dp"
android:layout_height="4dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" />
<android.support.v7.widget.RecyclerView
android:id="@+id/lists_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_shadow_shim" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</android.support.constraint.ConstraintLayout>

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.ModalTimelineActivity">
<include layout="@layout/toolbar_basic" />
<include
layout="@layout/toolbar_shadow_shim"
android:layout_width="0dp"
android:layout_height="4dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar" />
<FrameLayout
android:id="@+id/content_frame"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar_shadow_shim"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="?selectableItemBackground"
android:clickable="true">
<TextView
android:id="@+id/list_name_textview"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Example list" />
</android.support.constraint.ConstraintLayout>

@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:stateListAnimator="@null"

@ -1,13 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<View
android:id="@+id/toolbar_shadow_shim"
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="@drawable/material_drawer_shadow_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_collapseMode="pin" />
</merge>
<View xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar_shadow_shim"
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="@drawable/material_drawer_shadow_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_collapseMode="pin" />

@ -272,6 +272,9 @@
<string name="title_media">Media</string>
<string name="replying_to">Replying to @%s</string>
<string name="load_more_placeholder_text">load more</string>
<string name="action_lists">Lists</string>
<string name="title_lists">Lists</string>
<string name="title_list_timeline">List timeline</string>
</resources>

Loading…
Cancel
Save