From c0c73f5c06222ec20dac6d9711a67817fb236ffd Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Mon, 28 Jan 2019 19:02:31 +0100 Subject: [PATCH] Error artwork (#1000) * Add new Elephant Friend images. Use them in ListsActivity. * Add error images to AccountListFragment * Add error images to Timeline & Notifications fragment. Needs rework. * Introduce BackgroundMessageView. Use it in AccountList. * Use correct button style for BackgroundMessageView Co-Authored-By: charlag * Use BackgroundMessageView * Add BackgroundMessageView docs * Re-color and document elephants * Apply feedback, disable refresh when error is shown * Fix string typo --- .../com/keylesspalace/tusky/ListsActivity.kt | 77 +++++-- .../tusky/fragment/AccountListFragment.kt | 53 +++-- .../tusky/fragment/NotificationsFragment.java | 65 +++--- .../tusky/fragment/TimelineFragment.java | 61 ++++-- .../tusky/view/BackgroundMessageView.kt | 46 +++++ app/src/main/res/drawable/elephant_error.xml | 129 ++++++++++++ .../res/drawable/elephant_friend_empty.xml | 195 ++++++++++-------- .../main/res/drawable/elephant_offline.xml | 134 ++++++++++++ app/src/main/res/layout/activity_lists.xml | 15 +- .../main/res/layout/fragment_account_list.xml | 21 +- app/src/main/res/layout/fragment_timeline.xml | 16 +- .../res/layout/view_background_message.xml | 26 +++ app/src/main/res/values/strings.xml | 2 + 13 files changed, 652 insertions(+), 188 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt create mode 100644 app/src/main/res/drawable/elephant_error.xml create mode 100644 app/src/main/res/drawable/elephant_offline.xml create mode 100644 app/src/main/res/layout/view_background_message.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 7fbfe9c5..1196bb8e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -3,25 +3,31 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.appcompat.widget.Toolbar import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.ProgressBar import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.widget.Toolbar +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.LoadingState.* import com.keylesspalace.tusky.di.Injectable 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.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.iconics.IconicsDrawable +import kotlinx.android.synthetic.main.activity_lists.* import retrofit2.Call import retrofit2.Response +import java.io.IOException import java.lang.ref.WeakReference import javax.inject.Inject @@ -35,13 +41,17 @@ interface ListsView { } -data class State(val lists: List, val isLoading: Boolean) +enum class LoadingState { + INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER +} + +data class State(val lists: List, val loadingState: LoadingState) class ListsViewModel(private val api: MastodonApi) { private var _view: WeakReference? = null private val view: ListsView? get() = _view?.get() - private var state = State(listOf(), false) + private var state = State(listOf(), INITIAL) fun attach(view: ListsView) { this._view = WeakReference(view) @@ -57,17 +67,23 @@ class ListsViewModel(private val api: MastodonApi) { view?.openTimeline(id) } + fun retryLoading() { + loadIfNeeded() + } + private fun loadIfNeeded() { - if (state.isLoading || !state.lists.isEmpty()) return - updateState(state.copy(isLoading = false)) + if (state.loadingState == LOADING || !state.lists.isEmpty()) return + updateState(state.copy(loadingState = LOADING)) api.getLists().enqueue(object : retrofit2.Callback> { override fun onResponse(call: Call>, response: Response>) { - updateState(state.copy(lists = response.body() ?: listOf(), isLoading = false)) + updateState(state.copy(lists = response.body() ?: listOf(), loadingState = LOADED)) } - override fun onFailure(call: Call>, t: Throwable?) { - updateState(state.copy(isLoading = false)) + override fun onFailure(call: Call>, err: Throwable?) { + updateState(state.copy( + loadingState = if (err is IOException) ERROR_NETWORK else ERROR_OTHER + )) } }) } @@ -94,9 +110,6 @@ class ListsActivity : BaseActivity(), ListsView, Injectable { @Inject lateinit var mastodonApi: MastodonApi - private lateinit var recyclerView: RecyclerView - private lateinit var progressBar: ProgressBar - private lateinit var viewModel: ListsViewModel private val adapter = ListsAdapter() @@ -105,8 +118,6 @@ class ListsActivity : BaseActivity(), ListsView, Injectable { 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 @@ -116,9 +127,9 @@ class ListsActivity : BaseActivity(), ListsView, Injectable { bar.setDisplayShowHomeEnabled(true) } - recyclerView.adapter = adapter - recyclerView.layoutManager = LinearLayoutManager(this) - recyclerView.addItemDecoration( + listsRecycler.adapter = adapter + listsRecycler.layoutManager = LinearLayoutManager(this) + listsRecycler.addItemDecoration( DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) viewModel = lastNonConfigurationInstance as? ListsViewModel ?: ListsViewModel(mastodonApi) @@ -137,8 +148,30 @@ class ListsActivity : BaseActivity(), ListsView, Injectable { override fun update(state: State) { adapter.update(state.lists) - progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE - + progressBar.visibility = if (state.loadingState == LOADING) View.VISIBLE else View.GONE + when (state.loadingState) { + INITIAL, LOADING -> messageView.hide() + ERROR_NETWORK -> { + messageView.show() + messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + viewModel.retryLoading() + } + } + ERROR_OTHER -> { + messageView.show() + messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + viewModel.retryLoading() + } + } + LOADED -> + if (state.lists.isEmpty()) { + messageView.show() + messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, + null) + } else { + messageView.hide() + } + } } override fun openTimeline(listId: String) { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index 136c2763..8da8da58 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -16,24 +16,19 @@ package com.keylesspalace.tusky.fragment import android.os.Bundle -import com.google.android.material.snackbar.Snackbar -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup - +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.AccountListActivity.Type import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.AccountAdapter -import com.keylesspalace.tusky.adapter.BlocksAdapter -import com.keylesspalace.tusky.adapter.FollowAdapter -import com.keylesspalace.tusky.adapter.FollowRequestsAdapter -import com.keylesspalace.tusky.adapter.MutesAdapter +import com.keylesspalace.tusky.adapter.* import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship @@ -41,14 +36,15 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.view.EndlessOnScrollListener import kotlinx.android.synthetic.main.fragment_account_list.* - -import javax.inject.Inject - import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import java.io.IOException +import javax.inject.Inject class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { @@ -83,7 +79,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { divider.setDrawable(drawable) recyclerView.addItemDecoration(divider) - adapter = when(type) { + adapter = when (type) { Type.BLOCKS -> BlocksAdapter(this) Type.MUTES -> MutesAdapter(this) Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this) @@ -143,7 +139,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { val mutesAdapter = adapter as MutesAdapter val unmutedUser = mutesAdapter.removeItem(position) - if(unmutedUser != null) { + if (unmutedUser != null) { Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) .setAction(R.string.action_undo) { mutesAdapter.addItem(unmutedUser, position) @@ -193,7 +189,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { val blocksAdapter = adapter as BlocksAdapter val unblockedUser = blocksAdapter.removeItem(position) - if(unblockedUser != null) { + if (unblockedUser != null) { Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) .setAction(R.string.action_undo) { blocksAdapter.addItem(unblockedUser, position) @@ -311,11 +307,36 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { fetching = false + if (adapter.itemCount == 0) { + messageView.show() + messageView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + } else { + messageView.hide() + } } private fun onFetchAccountsFailure(exception: Exception) { fetching = false Log.e(TAG, "Fetch failure", exception) + + if (adapter.itemCount == 0) { + messageView.show() + if (exception is IOException) { + messageView.setup(R.drawable.elephant_offline, R.string.error_network) { + messageView.hide() + this.fetchAccounts(null) + } + } else { + messageView.setup(R.drawable.elephant_error, R.string.error_generic) { + messageView.hide() + this.fetchAccounts(null) + } + } + } } companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 90e79299..65f30f8a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -16,31 +16,19 @@ package com.keylesspalace.tusky.fragment; import android.app.Activity; -import androidx.arch.core.util.Function; -import androidx.lifecycle.Lifecycle; import android.content.Context; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.preference.PreferenceManager; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.tabs.TabLayout; -import androidx.core.util.Pair; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import androidx.appcompat.content.res.AppCompatResources; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.SimpleItemAnimator; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; -import android.widget.TextView; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.tabs.TabLayout; import com.keylesspalace.tusky.MainActivity; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.NotificationsAdapter; @@ -64,10 +52,12 @@ import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ViewDataUtils; +import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; +import java.io.IOException; import java.math.BigInteger; import java.util.Collections; import java.util.Iterator; @@ -76,7 +66,18 @@ import java.util.Objects; import javax.inject.Inject; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; +import androidx.core.util.Pair; +import androidx.lifecycle.Lifecycle; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import io.reactivex.android.schedulers.AndroidSchedulers; +import kotlin.Unit; import kotlin.collections.CollectionsKt; import retrofit2.Call; import retrofit2.Callback; @@ -125,7 +126,7 @@ public class NotificationsFragment extends SFragment implements private SwipeRefreshLayout swipeRefreshLayout; private RecyclerView recyclerView; private ProgressBar progressBar; - private TextView nothingMessageView; + private BackgroundMessageView statusView; private LinearLayoutManager layoutManager; private EndlessOnScrollListener scrollListener; @@ -177,7 +178,7 @@ public class NotificationsFragment extends SFragment implements swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout); recyclerView = rootView.findViewById(R.id.recycler_view); progressBar = rootView.findViewById(R.id.progress_bar); - nothingMessageView = rootView.findViewById(R.id.nothing_message); + statusView = rootView.findViewById(R.id.statusView); swipeRefreshLayout.setOnRefreshListener(this); swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); @@ -208,20 +209,12 @@ public class NotificationsFragment extends SFragment implements bottomId = null; ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); - setupNothingView(); sendFetchNotificationsRequest(null, null, FetchEnd.BOTTOM, -1); return rootView; } - private void setupNothingView() { - Drawable top = AppCompatResources.getDrawable(Objects.requireNonNull(getContext()), - R.drawable.elephant_friend_empty); - nothingMessageView.setCompoundDrawablesWithIntrinsicBounds(null, top, null, null); - nothingMessageView.setVisibility(View.GONE); - } - private void handleFavEvent(FavoriteEvent event) { Pair posAndNotification = findReplyPosition(event.getStatusId()); @@ -332,6 +325,8 @@ public class NotificationsFragment extends SFragment implements @Override public void onRefresh() { + swipeRefreshLayout.setEnabled(true); + this.statusView.setVisibility(View.GONE); Either first = CollectionsKt.firstOrNull(this.notifications); String topId; if (first != null && first.isRight()) { @@ -721,9 +716,9 @@ public class NotificationsFragment extends SFragment implements } if (notifications.size() == 0 && adapter.getItemCount() == 0) { - nothingMessageView.setVisibility(View.VISIBLE); - } else { - nothingMessageView.setVisibility(View.GONE); + this.statusView.setVisibility(View.VISIBLE); + this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); + } swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); @@ -736,6 +731,22 @@ public class NotificationsFragment extends SFragment implements new NotificationViewData.Placeholder(false); notifications.setPairedItem(position, placeholderVD); adapter.updateItemWithNotify(position, placeholderVD, true); + } else if (this.notifications.isEmpty()) { + this.statusView.setVisibility(View.VISIBLE); + swipeRefreshLayout.setEnabled(false); + if (exception instanceof IOException) { + this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { + this.progressBar.setVisibility(View.VISIBLE); + this.onRefresh(); + return Unit.INSTANCE; + }); + } else { + this.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> { + this.progressBar.setVisibility(View.VISIBLE); + this.onRefresh(); + return Unit.INSTANCE; + }); + } } Log.e(TAG, "Fetch failure: " + exception.getMessage()); progressBar.setVisibility(View.GONE); 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 ddf86be8..dba986fd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -25,7 +25,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; -import android.widget.TextView; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; @@ -56,9 +55,11 @@ import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ViewDataUtils; +import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.viewdata.StatusViewData; +import java.io.IOException; import java.math.BigInteger; import java.util.Iterator; import java.util.List; @@ -71,7 +72,6 @@ import javax.inject.Inject; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.content.res.AppCompatResources; import androidx.arch.core.util.Function; import androidx.core.util.Pair; import androidx.lifecycle.Lifecycle; @@ -86,6 +86,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import at.connyduck.sparkbutton.helpers.Utils; import io.reactivex.android.schedulers.AndroidSchedulers; +import kotlin.Unit; import kotlin.collections.CollectionsKt; import retrofit2.Call; import retrofit2.Callback; @@ -137,7 +138,7 @@ public class TimelineFragment extends SFragment implements private SwipeRefreshLayout swipeRefreshLayout; private RecyclerView recyclerView; private ProgressBar progressBar; - private TextView nothingMessageView; + private BackgroundMessageView statusView; private TimelineAdapter adapter; private Kind kind; @@ -220,13 +221,12 @@ public class TimelineFragment extends SFragment implements recyclerView = rootView.findViewById(R.id.recycler_view); swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout); progressBar = rootView.findViewById(R.id.progress_bar); - nothingMessageView = rootView.findViewById(R.id.nothing_message); + statusView = rootView.findViewById(R.id.statusView); setupSwipeRefreshLayout(); setupRecyclerView(); updateAdapter(); setupTimelinePreferences(); - setupNothingView(); if (statuses.isEmpty()) { progressBar.setVisibility(View.VISIBLE); @@ -376,10 +376,15 @@ public class TimelineFragment extends SFragment implements } } if (statuses.size() == 0) { - nothingMessageView.setVisibility(View.VISIBLE); + showNothing(); } } + private void showNothing() { + statusView.setVisibility(View.VISIBLE); + statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); + } + @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -502,14 +507,10 @@ public class TimelineFragment extends SFragment implements super.onDestroyView(); } - private void setupNothingView() { - Drawable top = AppCompatResources.getDrawable(requireContext(), R.drawable.elephant_friend_empty); - nothingMessageView.setCompoundDrawablesWithIntrinsicBounds(null, top, null, null); - nothingMessageView.setVisibility(View.GONE); - } - @Override public void onRefresh() { + swipeRefreshLayout.setEnabled(true); + this.statusView.setVisibility(View.GONE); if (this.initialUpdateFailed) { updateCurrent(); } else { @@ -863,7 +864,6 @@ public class TimelineFragment extends SFragment implements private void sendFetchTimelineRequest(@Nullable String fromId, @Nullable String uptoId, final FetchEnd fetchEnd, final int pos) { - if (kind == Kind.HOME) { TimelineRequestMode mode; // allow getting old statuses/fallbacks for network only for for bottom loading @@ -872,13 +872,13 @@ public class TimelineFragment extends SFragment implements } else { mode = TimelineRequestMode.NETWORK; } - timelineRepo.getStatuses(fromId, uptoId, LOAD_AT_ONCE, mode) - .observeOn(AndroidSchedulers.mainThread()) - .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - (result) -> onFetchTimelineSuccess(result, fetchEnd, pos), - (err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos) - ); + timelineRepo.getStatuses(fromId, uptoId, LOAD_AT_ONCE, mode) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + (result) -> onFetchTimelineSuccess(result, fetchEnd, pos), + (err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos) + ); } else { Callback> callback = new Callback>() { @Override @@ -946,10 +946,11 @@ public class TimelineFragment extends SFragment implements updateBottomLoadingState(fetchEnd); progressBar.setVisibility(View.GONE); swipeRefreshLayout.setRefreshing(false); + swipeRefreshLayout.setEnabled(true); if (this.statuses.size() == 0) { - nothingMessageView.setVisibility(View.VISIBLE); + this.showNothing(); } else { - nothingMessageView.setVisibility(View.GONE); + this.statusView.setVisibility(View.GONE); } } @@ -968,6 +969,22 @@ public class TimelineFragment extends SFragment implements newViewData = new StatusViewData.Placeholder(placeholder.getId(), false); statuses.setPairedItem(position, newViewData); updateAdapter(); + } else if (this.statuses.isEmpty()) { + swipeRefreshLayout.setEnabled(false); + this.statusView.setVisibility(View.VISIBLE); + if (exception instanceof IOException) { + this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { + this.progressBar.setVisibility(View.VISIBLE); + this.onRefresh(); + return Unit.INSTANCE; + }); + } else { + this.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> { + this.progressBar.setVisibility(View.VISIBLE); + this.onRefresh(); + return Unit.INSTANCE; + }); + } } Log.e(TAG, "Fetch Failure: " + exception.getMessage()); diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt new file mode 100644 index 00000000..1b73e6f7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -0,0 +1,46 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.View +import android.widget.LinearLayout +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.util.visible +import kotlinx.android.synthetic.main.view_background_message.view.* + + +/** + * This view is used for screens with downloadable content which may fail. + * Can show an image, text and button below them. + */ +class BackgroundMessageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + init { + View.inflate(context, R.layout.view_background_message, this) + gravity = Gravity.CENTER_HORIZONTAL + orientation = VERTICAL + + if (isInEditMode) { + setup(R.drawable.elephant_offline, R.string.error_network) {} + } + } + + /** + * Setup image, message and button. + * If [clickListener] is `null` then the button will be hidden. + */ + fun setup(@DrawableRes imageRes: Int, @StringRes messageRes: Int, + clickListener: ((v: View) -> Unit)?) { + messageTextView.setText(messageRes) + messageTextView.setCompoundDrawablesWithIntrinsicBounds(0, imageRes, 0, 0) + button.setOnClickListener(clickListener) + button.visible(clickListener != null) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/elephant_error.xml b/app/src/main/res/drawable/elephant_error.xml new file mode 100644 index 00000000..1c7948a9 --- /dev/null +++ b/app/src/main/res/drawable/elephant_error.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/elephant_friend_empty.xml b/app/src/main/res/drawable/elephant_friend_empty.xml index dba4aee3..1a67e093 100644 --- a/app/src/main/res/drawable/elephant_friend_empty.xml +++ b/app/src/main/res/drawable/elephant_friend_empty.xml @@ -3,92 +3,111 @@ android:height="159dp" android:viewportWidth="510" android:viewportHeight="580"> - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/elephant_offline.xml b/app/src/main/res/drawable/elephant_offline.xml new file mode 100644 index 00000000..2a5381a3 --- /dev/null +++ b/app/src/main/res/drawable/elephant_offline.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_lists.xml b/app/src/main/res/layout/activity_lists.xml index 772b1b11..44ccf5c8 100644 --- a/app/src/main/res/layout/activity_lists.xml +++ b/app/src/main/res/layout/activity_lists.xml @@ -8,7 +8,7 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_account_list.xml b/app/src/main/res/layout/fragment_account_list.xml index 591c4a9d..46149e30 100644 --- a/app/src/main/res/layout/fragment_account_list.xml +++ b/app/src/main/res/layout/fragment_account_list.xml @@ -1,6 +1,19 @@ - \ No newline at end of file + android:layout_height="match_parent"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_timeline.xml b/app/src/main/res/layout/fragment_timeline.xml index 383ee231..7a9381f7 100644 --- a/app/src/main/res/layout/fragment_timeline.xml +++ b/app/src/main/res/layout/fragment_timeline.xml @@ -1,6 +1,7 @@ @@ -25,16 +26,17 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/elephant_error" + tools:visibility="visible"/> + \ No newline at end of file diff --git a/app/src/main/res/layout/view_background_message.xml b/app/src/main/res/layout/view_background_message.xml new file mode 100644 index 00000000..41e4c85a --- /dev/null +++ b/app/src/main/res/layout/view_background_message.xml @@ -0,0 +1,26 @@ + + + + + +