From 6152043df3caaeb13d294035e3540308a77f108e Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Sat, 6 Jan 2018 21:01:37 +0300 Subject: [PATCH] Add basic lists support (#501) --- app/src/main/AndroidManifest.xml | 2 + .../com/keylesspalace/tusky/ListsActivity.kt | 199 ++++++++++++++++++ .../com/keylesspalace/tusky/MainActivity.java | 4 + .../tusky/ModalTimelineActivity.kt | 63 ++++++ .../keylesspalace/tusky/entity/MastoList.kt | 7 + .../tusky/fragment/TimelineFragment.java | 37 ++-- .../tusky/network/MastodonApi.java | 10 + app/src/main/res/layout/activity_lists.xml | 37 ++++ .../res/layout/activity_modal_timeline.xml | 27 +++ app/src/main/res/layout/item_list.xml | 21 ++ app/src/main/res/layout/toolbar_basic.xml | 1 + .../main/res/layout/toolbar_shadow_shim.xml | 20 +- app/src/main/res/values/strings.xml | 3 + 13 files changed, 405 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt create mode 100644 app/src/main/res/layout/activity_lists.xml create mode 100644 app/src/main/res/layout/activity_modal_timeline.xml create mode 100644 app/src/main/res/layout/item_list.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e46c52730..8f73899a0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,6 +95,8 @@ + + diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt new file mode 100644 index 000000000..da4aefbaf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -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, val isLoading: Boolean) + +class ListsViewModel(private val api: MastodonApi) { + + private var _view: WeakReference? = 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> { + override fun onResponse(call: Call>, response: Response>) { + updateState(state.copy(lists = response.body() ?: listOf(), isLoading = false)) + } + + override fun onFailure(call: Call>, 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(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() { + + private val items = mutableListOf() + + fun update(list: List) { + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index 6f46b09d1..aed7abe98 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -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 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)); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt new file mode 100644 index 000000000..fc81589ac --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -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(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 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt b/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt new file mode 100644 index 000000000..60b2bbc35 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 452120a9c..5276023fd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -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); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index c37bb4eb5..18ca3093f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -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> listTimeline( + @Path("listId") String listId, + @Query("max_id") String maxId, + @Query("since_id") String sinceId, + @Query("limit") Integer limit); @GET("api/v1/notifications") Call> notifications( @@ -236,4 +243,7 @@ public interface MastodonApi { Call statusCard( @Path("id") String statusId ); + + @GET("/api/v1/lists") + Call> getLists(); } diff --git a/app/src/main/res/layout/activity_lists.xml b/app/src/main/res/layout/activity_lists.xml new file mode 100644 index 000000000..4eaccefca --- /dev/null +++ b/app/src/main/res/layout/activity_lists.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_modal_timeline.xml b/app/src/main/res/layout/activity_modal_timeline.xml new file mode 100644 index 000000000..997a39a13 --- /dev/null +++ b/app/src/main/res/layout/activity_modal_timeline.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/item_list.xml b/app/src/main/res/layout/item_list.xml new file mode 100644 index 000000000..b1ea23ba4 --- /dev/null +++ b/app/src/main/res/layout/item_list.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_basic.xml b/app/src/main/res/layout/toolbar_basic.xml index 1086ee28d..fd09a276c 100644 --- a/app/src/main/res/layout/toolbar_basic.xml +++ b/app/src/main/res/layout/toolbar_basic.xml @@ -3,6 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2c20f6da..75f0ff411 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -272,6 +272,9 @@ Media Replying to @%s load more + Lists + Lists + List timeline