Use blurhash as image preview and as sensitive media cover, close #1571 (#1581)

* Use blurhash as image preview and as sensitive media cover, close #1571

* Fix focal point for blurhashes

* Fix video indicator overlapping sensitive media indicator

* Add a preference for blurhash

* Add blurhash to report UI.

* Introduce StatusDisplayOptions
This commit is contained in:
Ivan Kupalov 2019-12-30 21:37:20 +01:00 committed by Konrad Pozniak
commit 7623962a0d
32 changed files with 560 additions and 368 deletions

View file

@ -12,20 +12,21 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
class ConversationAdapter(private val useAbsoluteTime: Boolean,
private val mediaPreviewEnabled: Boolean,
private val listener: StatusActionListener,
private val topLoadedCallback: () -> Unit,
private val retryCallback: () -> Unit)
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class ConversationAdapter(
private val statusDisplayOptions: StatusDisplayOptions,
private val listener: StatusActionListener,
private val topLoadedCallback: () -> Unit,
private val retryCallback: () -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var networkState: NetworkState? = null
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object: ListUpdateCallback {
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
notifyItemRangeInserted(position, count)
if(position == 0) {
if (position == 0) {
topLoadedCallback()
}
}
@ -51,7 +52,8 @@ class ConversationAdapter(private val useAbsoluteTime: Boolean,
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
R.layout.item_network_state -> NetworkStateViewHolder(view, retryCallback)
R.layout.item_conversation -> ConversationViewHolder(view, listener, useAbsoluteTime, mediaPreviewEnabled)
R.layout.item_conversation -> ConversationViewHolder(view, statusDisplayOptions,
listener)
else -> throw IllegalArgumentException("unknown view type $viewType")
}
}

View file

@ -23,7 +23,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
@ -32,6 +31,7 @@ import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
import java.util.List;
@ -44,15 +44,13 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
private ToggleButton contentCollapseButton;
private ImageView[] avatars;
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener listener;
private boolean mediaPreviewEnabled;
private boolean animateAvatars;
ConversationViewHolder(View itemView,
StatusActionListener listener,
boolean useAbsoluteTime,
boolean mediaPreviewEnabled) {
super(itemView, useAbsoluteTime);
StatusDisplayOptions statusDisplayOptions,
StatusActionListener listener) {
super(itemView);
conversationNameTextView = itemView.findViewById(R.id.conversation_name);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
avatars = new ImageView[]{
@ -60,11 +58,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
itemView.findViewById(R.id.status_avatar_1),
itemView.findViewById(R.id.status_avatar_2)
};
this.statusDisplayOptions = statusDisplayOptions;
this.listener = listener;
this.mediaPreviewEnabled = mediaPreviewEnabled;
this.animateAvatars = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()).getBoolean("animateGifAvatars", false);
}
@Override
@ -86,8 +83,9 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setBookmarked(status.getBookmarked());
List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.getSensitive();
if(mediaPreviewEnabled && !hasAudioAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent());
if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(),
statusDisplayOptions.useBlurhash());
if (attachments.size() == 0) {
hideSensitiveMediaWarning();
@ -118,11 +116,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
private void setConversationName(List<ConversationAccountEntity> accounts) {
Context context = conversationNameTextView.getContext();
String conversationName = "";
if(accounts.size() == 1) {
if (accounts.size() == 1) {
conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername());
} else if(accounts.size() == 2) {
} else if (accounts.size() == 2) {
conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername());
} else if (accounts.size() > 2){
} else if (accounts.size() > 2) {
conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2);
}
@ -130,10 +128,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
}
private void setAvatars(List<ConversationAccountEntity> accounts) {
for(int i=0; i < avatars.length; i++) {
for (int i = 0; i < avatars.length; i++) {
ImageView avatarView = avatars[i];
if(i < accounts.size()) {
ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView, avatarRadius48dp, animateAvatars);
if (i < accounts.size()) {
ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView,
avatarRadius48dp, statusDisplayOptions.animateAvatars());
avatarView.setVisibility(View.VISIBLE);
} else {
avatarView.setVisibility(View.GONE);

View file

@ -37,6 +37,7 @@ import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.fragment_timeline.*
@ -62,15 +63,18 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
val account = accountManager.activeAccount
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true)
)
adapter = ConversationAdapter(useAbsoluteTime, mediaPreviewEnabled, this, ::onTopLoaded, viewModel::retry)
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(view.context)

View file

@ -31,12 +31,13 @@ import com.keylesspalace.tusky.viewdata.toViewData
import kotlinx.android.synthetic.main.item_report_status.view.*
import java.util.*
class StatusViewHolder(itemView: View,
private val useAbsoluteTime: Boolean,
private val mediaPreviewEnabled: Boolean,
private val viewState: StatusViewState,
private val adapterHandler: AdapterHandler,
private val getStatusForPosition: (Int) -> Status?) : RecyclerView.ViewHolder(itemView) {
class StatusViewHolder(
itemView: View,
private val statusDisplayOptions: StatusDisplayOptions,
private val viewState: StatusViewState,
private val adapterHandler: AdapterHandler,
private val getStatusForPosition: (Int) -> Status?
) : RecyclerView.ViewHolder(itemView) {
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
private val statusViewHelper = StatusViewHelper(itemView)
@ -69,11 +70,11 @@ class StatusViewHolder(itemView: View,
val sensitive = status.sensitive
statusViewHelper.setMediasPreview(mediaPreviewEnabled, status.attachments, sensitive, previewListener,
viewState.isMediaShow(status.id, status.sensitive),
statusViewHelper.setMediasPreview(statusDisplayOptions, status.attachments,
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
mediaViewHeight)
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, useAbsoluteTime)
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions.useAbsoluteTime)
setCreatedAt(status.createdAt)
}
@ -124,7 +125,7 @@ class StatusViewHolder(itemView: View,
}
private fun setCreatedAt(createdAt: Date?) {
if (useAbsoluteTime) {
if (statusDisplayOptions.useAbsoluteTime) {
itemView.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt)
} else {
itemView.timestampInfo.text = if (createdAt != null) {

View file

@ -23,12 +23,13 @@ import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.model.StatusViewState
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions
class StatusesAdapter(private val useAbsoluteTime: Boolean,
private val mediaPreviewEnabled: Boolean,
private val statusViewState: StatusViewState,
private val adapterHandler: AdapterHandler)
: PagedListAdapter<Status, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
class StatusesAdapter(
private val statusDisplayOptions: StatusDisplayOptions,
private val statusViewState: StatusViewState,
private val adapterHandler: AdapterHandler
) : PagedListAdapter<Status, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
private val statusForPosition: (Int) -> Status? = { position: Int ->
if (position != RecyclerView.NO_POSITION) getItem(position) else null
@ -36,8 +37,10 @@ class StatusesAdapter(private val useAbsoluteTime: Boolean,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return StatusViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_report_status, parent, false),
useAbsoluteTime, mediaPreviewEnabled, statusViewState, adapterHandler, statusForPosition)
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_report_status, parent, false)
return StatusViewHolder(view, statusDisplayOptions, statusViewState, adapterHandler,
statusForPosition)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {

View file

@ -43,6 +43,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
@ -119,14 +120,16 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
private fun initStatusesView() {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = false,
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = false,
useBlurhash = preferences.getBoolean("useBlurhash", true)
)
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
val account = accountManager.activeAccount
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
adapter = StatusesAdapter(useAbsoluteTime, mediaPreviewEnabled, viewModel.statusViewState, this)
adapter = StatusesAdapter(statusDisplayOptions,
viewModel.statusViewState, this)
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(requireContext())

View file

@ -24,28 +24,26 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.StatusViewHolder
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.StatusViewData
class SearchStatusesAdapter(private val useAbsoluteTime: Boolean,
private val mediaPreviewEnabled: Boolean,
private val showBotOverlay: Boolean,
private val animateAvatar: Boolean,
private val statusListener: StatusActionListener)
: PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
class SearchStatusesAdapter(
private val statusDisplayOptions: StatusDisplayOptions,
private val statusListener: StatusActionListener
) : PagedListAdapter<Pair<Status, StatusViewData.Concrete>, RecyclerView.ViewHolder>(STATUS_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_status, parent, false)
return StatusViewHolder(view, useAbsoluteTime)
return StatusViewHolder(view)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item ->
(holder as? StatusViewHolder)?.setupWithStatus(item.second, statusListener,
mediaPreviewEnabled, showBotOverlay, animateAvatar)
statusDisplayOptions)
}
}
public override fun getItem(position: Int): Pair<Status, StatusViewData.Concrete>? {

View file

@ -52,6 +52,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
@ -71,13 +72,17 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
val showBotOverlay = preferences.getBoolean("showBotOverlay", true)
val animateAvatar = preferences.getBoolean("animateGifAvatars", false)
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true)
)
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context)
return SearchStatusesAdapter(useAbsoluteTime, viewModel.mediaPreviewEnabled, showBotOverlay, animateAvatar, this)
return SearchStatusesAdapter(statusDisplayOptions, this)
}