Add basic lists support (#501)

This commit is contained in:
Ivan Kupalov 2018-01-06 21:01:37 +03:00 committed by Konrad Pozniak
parent dd9bba94bb
commit 6152043df3
13 changed files with 405 additions and 26 deletions

View file

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

View file

@ -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)
}
}
}
}

View file

@ -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_LOG_OUT = 7;
private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 8; private static final long DRAWER_ITEM_FOLLOW_REQUESTS = 8;
private static final long DRAWER_ITEM_SAVED_TOOT = 9; private static final long DRAWER_ITEM_SAVED_TOOT = 9;
private static final long DRAWER_ITEM_LISTS = 10;
private static int COMPOSE_RESULT = 1; private static int COMPOSE_RESULT = 1;
@ -311,6 +312,7 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity {
List<IDrawerItem> listItem = new ArrayList<>(); 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_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_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_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_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)); 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) { } else if (drawerItemIdentifier == DRAWER_ITEM_SAVED_TOOT) {
Intent intent = new Intent(MainActivity.this, SavedTootActivity.class); Intent intent = new Intent(MainActivity.this, SavedTootActivity.class);
startActivity(intent); startActivity(intent);
} else if (drawerItemIdentifier == DRAWER_ITEM_LISTS) {
startActivity(ListsActivity.newIntent(this));
} }
} }

View file

@ -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
}
}

View file

@ -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)

View file

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

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

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

View file

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

View file

@ -272,6 +272,9 @@
<string name="title_media">Media</string> <string name="title_media">Media</string>
<string name="replying_to">Replying to @%s</string> <string name="replying_to">Replying to @%s</string>
<string name="load_more_placeholder_text">load more</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> </resources>