Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2020-01-05 01:02:28 +09:00
commit 9efc4a4bc9
59 changed files with 1211 additions and 893 deletions

View File

@ -137,7 +137,7 @@
android:name=".components.report.ReportActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity android:name=".components.instancemute.InstanceListActivity" />
<activity android:name=".ScheduledTootActivity" />
<activity android:name=".components.scheduled.ScheduledTootActivity" />
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
<receiver

View File

@ -54,6 +54,7 @@ import com.keylesspalace.tusky.appstore.MainTabsChangedEvent;
import com.keylesspalace.tusky.appstore.ProfileEditedEvent;
import com.keylesspalace.tusky.components.compose.ComposeActivity;
import com.keylesspalace.tusky.components.conversation.ConversationsRepository;
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity;
import com.keylesspalace.tusky.components.search.SearchActivity;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.entity.Account;

View File

@ -56,7 +56,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
setDisplayShowHomeEnabled(true)
}
val fragment: Fragment = when(intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
val fragment: Fragment = when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance()
@ -128,7 +128,8 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
this.restartCurrentActivity()
}
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "viewPagerOffScreenLimit" -> {
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash", "viewPagerOffScreenLimit" -> {
restartActivitiesOnExit = true
}
"language" -> {

View File

@ -1,178 +0,0 @@
package com.keylesspalace.tusky
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.keylesspalace.tusky.adapter.ScheduledTootAdapter
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.uber.autodispose.AutoDispose.autoDisposable
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_scheduled_toot.*
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import javax.inject.Inject
class ScheduledTootActivity : BaseActivity(), ScheduledTootAdapter.ScheduledTootAction, Injectable {
companion object {
@JvmStatic
fun newIntent(context: Context): Intent {
return Intent(context, ScheduledTootActivity::class.java)
}
}
lateinit var adapter: ScheduledTootAdapter
@Inject
lateinit var mastodonApi: MastodonApi
@Inject
lateinit var eventHub: EventHub
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scheduled_toot)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
val bar = supportActionBar
if (bar != null) {
bar.title = getString(R.string.title_scheduled_toot)
bar.setDisplayHomeAsUpEnabled(true)
bar.setDisplayShowHomeEnabled(true)
}
swipe_refresh_layout.setOnRefreshListener(this::refreshStatuses)
scheduled_toot_list.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(this)
scheduled_toot_list.layoutManager = layoutManager
val divider = DividerItemDecoration(this, layoutManager.orientation)
scheduled_toot_list.addItemDecoration(divider)
adapter = ScheduledTootAdapter(this)
scheduled_toot_list.adapter = adapter
loadStatuses()
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
.`as`(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe { event ->
if (event is StatusScheduledEvent) {
refreshStatuses()
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
fun loadStatuses() {
progress_bar.visibility = View.VISIBLE
mastodonApi.scheduledStatuses()
.enqueue(object : Callback<List<ScheduledStatus>> {
override fun onResponse(call: Call<List<ScheduledStatus>>, response: Response<List<ScheduledStatus>>) {
progress_bar.visibility = View.GONE
if (response.body().isNullOrEmpty()) {
errorMessageView.show()
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
null)
} else {
show(response.body()!!)
}
}
override fun onFailure(call: Call<List<ScheduledStatus>>, t: Throwable) {
progress_bar.visibility = View.GONE
errorMessageView.show()
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
errorMessageView.hide()
loadStatuses()
}
}
})
}
private fun refreshStatuses() {
swipe_refresh_layout.isRefreshing = true
mastodonApi.scheduledStatuses()
.enqueue(object : Callback<List<ScheduledStatus>> {
override fun onResponse(call: Call<List<ScheduledStatus>>, response: Response<List<ScheduledStatus>>) {
swipe_refresh_layout.isRefreshing = false
if (response.body().isNullOrEmpty()) {
errorMessageView.show()
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
null)
} else {
show(response.body()!!)
}
}
override fun onFailure(call: Call<List<ScheduledStatus>>, t: Throwable) {
swipe_refresh_layout.isRefreshing = false
}
})
}
fun show(statuses: List<ScheduledStatus>) {
adapter.setItems(statuses)
adapter.notifyDataSetChanged()
}
override fun edit(position: Int, item: ScheduledStatus?) {
if (item == null) {
return
}
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
tootText = item.params.text,
contentWarning = item.params.spoilerText,
mediaAttachments = item.mediaAttachments,
inReplyToId = item.params.inReplyToId,
visibility = item.params.visibility,
scheduledAt = item.scheduledAt,
sensitive = item.params.sensitive
))
startActivity(intent)
delete(position, item)
}
override fun delete(position: Int, item: ScheduledStatus?) {
if (item == null) {
return
}
mastodonApi.deleteScheduledStatus(item.id)
.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
adapter.removeItem(position)
}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
}
})
}
}

View File

@ -48,10 +48,11 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.viewdata.NotificationViewData;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import com.mikepenz.iconics.utils.Utils;
@ -82,28 +83,23 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
private String accountId;
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
private BidiFormatter bidiFormatter;
private AdapterDataSource<NotificationViewData> dataSource;
public NotificationsAdapter(String accountId,
AdapterDataSource<NotificationViewData> dataSource,
StatusDisplayOptions statusDisplayOptions,
StatusActionListener statusListener,
NotificationActionListener notificationActionListener) {
this.accountId = accountId;
this.dataSource = dataSource;
this.statusDisplayOptions = statusDisplayOptions;
this.statusListener = statusListener;
this.notificationActionListener = notificationActionListener;
mediaPreviewEnabled = true;
useAbsoluteTime = false;
showBotOverlay = true;
animateAvatar = false;
bidiFormatter = BidiFormatter.getInstance();
}
@ -112,20 +108,20 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case VIEW_TYPE_STATUS: {
case VIEW_TYPE_STATUS: {
View view = inflater
.inflate(R.layout.item_status, parent, false);
return new StatusViewHolder(view, useAbsoluteTime);
return new StatusViewHolder(view);
}
case VIEW_TYPE_STATUS_NOTIFICATION: {
View view = inflater
.inflate(R.layout.item_status_notification, parent, false);
return new StatusNotificationViewHolder(view, useAbsoluteTime, animateAvatar);
return new StatusNotificationViewHolder(view, statusDisplayOptions);
}
case VIEW_TYPE_FOLLOW: {
View view = inflater
.inflate(R.layout.item_follow, parent, false);
return new FollowViewHolder(view, animateAvatar);
return new FollowViewHolder(view, statusDisplayOptions);
}
case VIEW_TYPE_PLACEHOLDER: {
View view = inflater
@ -141,7 +137,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
Utils.convertDpToPx(parent.getContext(), 24)
)
);
return new RecyclerView.ViewHolder(view) {};
return new RecyclerView.ViewHolder(view) {
};
}
}
}
@ -175,8 +172,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
holder.setupWithStatus(status,
statusListener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloadForHolder);
if(concreteNotificaton.getType() == Notification.Type.POLL) {
statusListener, statusDisplayOptions, payloadForHolder);
if (concreteNotificaton.getType() == Notification.Type.POLL) {
holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId()));
} else {
holder.hideStatusInfo();
@ -206,7 +203,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
concreteNotificaton.getId());
} else {
if (payloadForHolder instanceof List)
for (Object item : (List)payloadForHolder) {
for (Object item : (List) payloadForHolder) {
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item)) {
holder.setCreatedAt(statusViewData.getCreatedAt());
}
@ -225,7 +222,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
default:
}
}
}
@Override
@ -233,6 +229,20 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
return dataSource.getItemCount();
}
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
this.statusDisplayOptions = statusDisplayOptions.copy(
statusDisplayOptions.animateAvatars(),
mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash()
);
}
public boolean isMediaPreviewEnabled() {
return this.statusDisplayOptions.mediaPreviewEnabled();
}
@Override
public int getItemViewType(int position) {
NotificationViewData notification = dataSource.getItemAt(position);
@ -260,26 +270,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
throw new AssertionError("Unknown notification type");
}
}
public void setMediaPreviewEnabled(boolean enabled) {
mediaPreviewEnabled = enabled;
}
public boolean isMediaPreviewEnabled() {
return mediaPreviewEnabled;
}
public void setUseAbsoluteTime(boolean useAbsoluteTime) {
this.useAbsoluteTime = useAbsoluteTime;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
}
public interface NotificationActionListener {
@ -304,15 +295,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private TextView usernameView;
private TextView displayNameView;
private ImageView avatar;
private boolean animateAvatar;
private StatusDisplayOptions statusDisplayOptions;
FollowViewHolder(View itemView, boolean animateAvatar) {
FollowViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
super(itemView);
message = itemView.findViewById(R.id.notification_text);
usernameView = itemView.findViewById(R.id.notification_username);
displayNameView = itemView.findViewById(R.id.notification_display_name);
avatar = itemView.findViewById(R.id.notification_avatar);
this.animateAvatar = animateAvatar;
this.statusDisplayOptions = statusDisplayOptions;
}
void setMessage(Account account, BidiFormatter bidiFormatter) {
@ -334,7 +325,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_42dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius,
statusDisplayOptions.animateAvatars());
}
@ -357,18 +349,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private final ToggleButton contentWarningButton;
private final ToggleButton contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
private ConstraintLayout quoteContainer;
private StatusDisplayOptions statusDisplayOptions;
private String accountId;
private String notificationId;
private NotificationActionListener notificationActionListener;
private StatusViewData.Concrete statusViewData;
private boolean useAbsoluteTime;
private boolean animateAvatar;
private SimpleDateFormat shortSdf;
private SimpleDateFormat longSdf;
StatusNotificationViewHolder(View itemView, boolean useAbsoluteTime, boolean animateAvatar) {
StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
super(itemView);
message = itemView.findViewById(R.id.notification_top_text);
statusNameBar = itemView.findViewById(R.id.status_name_bar);
@ -381,6 +371,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
contentWarningDescriptionTextView = itemView.findViewById(R.id.notification_content_warning_description);
contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
this.statusDisplayOptions = statusDisplayOptions;
quoteContainer = itemView.findViewById(R.id.status_quote_inline_container);
@ -392,9 +383,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
message.setOnClickListener(this);
statusContent.setOnClickListener(this);
contentWarningButton.setOnCheckedChangeListener(this);
this.useAbsoluteTime = useAbsoluteTime;
this.animateAvatar = animateAvatar;
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
}
@ -421,7 +409,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
protected void setCreatedAt(@Nullable Date createdAt) {
if (useAbsoluteTime) {
if (statusDisplayOptions.useAbsoluteTime()) {
String time;
if (createdAt != null) {
if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
@ -518,13 +506,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.getDimensionPixelSize(R.dimen.avatar_radius_36dp);
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
statusAvatar, statusAvatarRadius, animateAvatar);
statusAvatar, statusAvatarRadius, statusDisplayOptions.animateAvatars());
int notificationAvatarRadius = statusAvatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_24dp);
ImageLoadingHelper.loadAvatar(notificationAvatarUrl,
notificationAvatar, notificationAvatarRadius, animateAvatar);
ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar,
notificationAvatarRadius, statusDisplayOptions.animateAvatars());
}
private void setQuoteContainer(Status status, final LinkListener listener) {

View File

@ -1,125 +0,0 @@
/* Copyright 2019 kyori19
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.ScheduledStatus;
import java.util.ArrayList;
import java.util.List;
public class ScheduledTootAdapter extends RecyclerView.Adapter {
private List<ScheduledStatus> list;
private ScheduledTootAction handler;
public ScheduledTootAdapter(Context context) {
super();
list = new ArrayList<>();
handler = (ScheduledTootAction) context;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_scheduled_toot, parent, false);
return new TootViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
TootViewHolder holder = (TootViewHolder) viewHolder;
holder.bind(getItem(position));
}
@Override
public int getItemCount() {
return list.size();
}
public void setItems(List<ScheduledStatus> newToot) {
list = new ArrayList<>();
list.addAll(newToot);
}
@Nullable
public ScheduledStatus removeItem(int position) {
if (position < 0 || position >= list.size()) {
return null;
}
ScheduledStatus toot = list.remove(position);
notifyItemRemoved(position);
return toot;
}
private ScheduledStatus getItem(int position) {
if (position >= 0 && position < list.size()) {
return list.get(position);
}
return null;
}
public interface ScheduledTootAction {
void edit(int position, ScheduledStatus item);
void delete(int position, ScheduledStatus item);
}
private class TootViewHolder extends RecyclerView.ViewHolder {
View view;
TextView text;
ImageButton edit;
ImageButton delete;
TootViewHolder(View view) {
super(view);
this.view = view;
this.text = view.findViewById(R.id.text);
this.edit = view.findViewById(R.id.edit);
this.delete = view.findViewById(R.id.delete);
}
void bind(final ScheduledStatus item) {
edit.setEnabled(true);
delete.setEnabled(true);
if (item != null) {
text.setText(item.getParams().getText());
edit.setOnClickListener(v -> {
v.setEnabled(false);
handler.edit(getAdapterPosition(), item);
});
delete.setOnClickListener(v -> {
v.setEnabled(false);
handler.delete(getAdapterPosition(), item);
});
}
}
}
}

View File

@ -3,6 +3,8 @@ package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.text.Spanned;
import android.text.TextUtils;
@ -35,6 +37,7 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.HtmlUtils;
import com.keylesspalace.tusky.util.ImageLoadingHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.view.MediaPreviewImageView;
@ -54,7 +57,6 @@ import java.util.Locale;
import java.util.Objects;
import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.SparkEventListener;
import kotlin.collections.CollectionsKt;
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
@ -93,7 +95,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private PollAdapter pollAdapter;
private boolean useAbsoluteTime;
private SimpleDateFormat shortSdf;
private SimpleDateFormat longSdf;
@ -103,10 +104,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private int avatarRadius36dp;
private int avatarRadius24dp;
private final int mediaPreviewUnloadedId;
private final Drawable mediaPreviewUnloaded;
protected StatusBaseViewHolder(View itemView,
boolean useAbsoluteTime) {
protected StatusBaseViewHolder(View itemView) {
super(itemView);
displayName = itemView.findViewById(R.id.status_display_name);
username = itemView.findViewById(R.id.status_username);
@ -120,6 +120,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
bookmarkButton = itemView.findViewById(R.id.status_bookmark);
moreButton = itemView.findViewById(R.id.status_more);
itemView.findViewById(R.id.status_media_preview_container).setClipToOutline(true);
mediaPreviews = new MediaPreviewImageView[]{
itemView.findViewById(R.id.status_media_preview_0),
itemView.findViewById(R.id.status_media_preview_1),
@ -155,7 +157,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false);
this.useAbsoluteTime = useAbsoluteTime;
this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
@ -163,8 +164,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
mediaPreviewUnloadedId = ThemeUtils.getDrawableId(itemView.getContext(),
R.attr.media_preview_unloaded_drawable, android.R.color.black);
mediaPreviewUnloaded = itemView.getContext().getDrawable(
ThemeUtils.getDrawableId(itemView.getContext(),
R.attr.media_preview_unloaded_drawable, android.R.color.black)
);
}
protected abstract int getMediaPreviewHeight(Context context);
@ -190,12 +193,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
@Nullable Status.Mention[] mentions,
@NonNull List<Emoji> emojis,
@Nullable PollViewData poll,
@NonNull StatusDisplayOptions statusDisplayOptions,
final StatusActionListener listener,
boolean removeQuote) {
if (TextUtils.isEmpty(spoilerText)) {
contentWarningDescription.setVisibility(View.GONE);
contentWarningButton.setVisibility(View.GONE);
this.setTextVisible(true, content, mentions, emojis, poll, listener, removeQuote);
this.setTextVisible(true, content, mentions, emojis, poll, statusDisplayOptions, listener, removeQuote);
} else {
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
contentWarningDescription.setText(emojiSpoiler);
@ -207,9 +211,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onExpandedChange(isChecked, getAdapterPosition());
}
this.setTextVisible(isChecked, content, mentions, emojis, poll, listener, removeQuote);
this.setTextVisible(isChecked, content, mentions, emojis, poll, statusDisplayOptions, listener, removeQuote);
});
this.setTextVisible(expanded, content, mentions, emojis, poll, listener, removeQuote);
this.setTextVisible(expanded, content, mentions, emojis, poll, statusDisplayOptions, listener, removeQuote);
}
}
@ -218,15 +222,19 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Status.Mention[] mentions,
List<Emoji> emojis,
@Nullable PollViewData poll,
StatusDisplayOptions statusDisplayOptions,
final StatusActionListener listener,
boolean removeQuote) {
if (expanded) {
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener, removeQuote);
if (poll != null) {
setupPoll(poll, emojis, listener);
setupPoll(poll, emojis, statusDisplayOptions, listener);
} else {
hidePoll();
}
} else {
hidePoll();
LinkHelper.setClickableMentions(this.content, mentions, listener);
}
if (TextUtils.isEmpty(this.content.getText())) {
@ -234,27 +242,24 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} else {
this.content.setVisibility(View.VISIBLE);
}
setPollVisible(poll != null && expanded);
}
private void setPollVisible(boolean visible) {
int visibility = visible ? View.VISIBLE : View.GONE;
pollButton.setVisibility(visibility);
pollDescription.setVisibility(visibility);
pollOptions.setVisibility(visibility);
private void hidePoll() {
pollButton.setVisibility(View.GONE);
pollDescription.setVisibility(View.GONE);
pollOptions.setVisibility(View.GONE);
}
private void setAvatar(String url,
@Nullable String rebloggedUrl,
boolean isBot,
boolean showBotOverlay,
boolean animateAvatar) {
StatusDisplayOptions statusDisplayOptions) {
int avatarRadius;
if (TextUtils.isEmpty(rebloggedUrl)) {
avatar.setPaddingRelative(0, 0, 0, 0);
if (showBotOverlay && isBot) {
if (statusDisplayOptions.showBotOverlay() && isBot) {
avatarInset.setVisibility(View.VISIBLE);
avatarInset.setBackgroundColor(0x50ffffff);
Glide.with(avatarInset)
@ -273,20 +278,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
avatarInset.setVisibility(View.VISIBLE);
avatarInset.setBackground(null);
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp, animateAvatar);
ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp,
statusDisplayOptions.animateAvatars());
avatarRadius = avatarRadius36dp;
}
ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius, animateAvatar);
ImageLoadingHelper.loadAvatar(url, avatar, avatarRadius,
statusDisplayOptions.animateAvatars());
}
protected void setCreatedAt(Date createdAt) {
if (useAbsoluteTime) {
protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
if (statusDisplayOptions.useAbsoluteTime()) {
timestampInfo.setText(getAbsoluteTime(createdAt));
} else {
if(createdAt == null) {
if (createdAt == null) {
timestampInfo.setText("?m");
} else {
long then = createdAt.getTime();
@ -298,7 +305,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
private String getAbsoluteTime(Date createdAt) {
if(createdAt == null) {
if (createdAt == null) {
return "??:??:??";
}
if (DateUtils.isToday(createdAt.getTime())) {
@ -308,14 +315,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
private CharSequence getCreatedAtDescription(Date createdAt) {
if (useAbsoluteTime) {
private CharSequence getCreatedAtDescription(Date createdAt,
StatusDisplayOptions statusDisplayOptions) {
if (statusDisplayOptions.useAbsoluteTime()) {
return getAbsoluteTime(createdAt);
} else {
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
* as 17 meters instead of minutes. */
if(createdAt == null) {
if (createdAt == null) {
return "? minutes";
} else {
long then = createdAt.getTime();
@ -460,12 +468,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
bookmarkButton.setChecked(bookmarked);
}
private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta) {
private BitmapDrawable decodeBlurHash(String blurhash) {
return ImageLoadingHelper.decodeBlurHash(this.avatar.getContext(), blurhash);
}
private void loadImage(MediaPreviewImageView imageView, String previewUrl, MetaData meta,
@Nullable String blurhash) {
Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded;
if (TextUtils.isEmpty(previewUrl)) {
Glide.with(imageView)
.load(mediaPreviewUnloadedId)
.centerInside()
.into(imageView);
if (blurhash != null) {
imageView.setImageDrawable(decodeBlurHash(blurhash));
} else {
Glide.with(imageView)
.load(placeholder)
.centerInside()
.into(imageView);
}
} else {
Focus focus = meta != null ? meta.getFocus() : null;
@ -474,7 +492,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Glide.with(imageView)
.load(previewUrl)
.placeholder(mediaPreviewUnloadedId)
.placeholder(placeholder)
.centerInside()
.addListener(imageView)
.into(imageView);
@ -483,7 +501,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Glide.with(imageView)
.load(previewUrl)
.placeholder(mediaPreviewUnloadedId)
.placeholder(placeholder)
.centerInside()
.into(imageView);
}
@ -491,39 +509,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
final StatusActionListener listener, boolean showingContent) {
final StatusActionListener listener, boolean showingContent,
boolean useBlurhash) {
Context context = itemView.getContext();
final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS);
for (int i = 0; i < n; i++) {
String previewUrl = attachments.get(i).getPreviewUrl();
String description = attachments.get(i).getDescription();
MediaPreviewImageView imageView = mediaPreviews[i];
imageView.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(description)) {
imageView.setContentDescription(imageView.getContext()
.getString(R.string.action_view_media));
} else {
imageView.setContentDescription(description);
}
if (!sensitive || showingContent) {
loadImage(imageView, previewUrl, attachments.get(i).getMeta());
} else {
imageView.setImageResource(mediaPreviewUnloadedId);
}
final Attachment.Type type = attachments.get(i).getType();
if (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV) {
mediaOverlays[i].setVisibility(View.VISIBLE);
} else {
mediaOverlays[i].setVisibility(View.GONE);
}
setAttachmentClickListener(imageView, listener, i, attachments.get(i), true);
}
final int mediaPreviewHeight = getMediaPreviewHeight(context);
@ -537,15 +527,51 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaPreviews[3].getLayoutParams().height = mediaPreviewHeight;
}
for (int i = 0; i < n; i++) {
Attachment attachment = attachments.get(i);
String previewUrl = attachment.getPreviewUrl();
String description = attachment.getDescription();
MediaPreviewImageView imageView = mediaPreviews[i];
imageView.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(description)) {
imageView.setContentDescription(imageView.getContext()
.getString(R.string.action_view_media));
} else {
imageView.setContentDescription(description);
}
if (showingContent) {
loadImage(imageView, previewUrl, attachment.getMeta(), attachment.getBlurhash());
} else {
imageView.setFocalPoint(null);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
if (useBlurhash && attachment.getBlurhash() != null) {
BitmapDrawable blurhashBitmap = decodeBlurHash(attachment.getBlurhash());
imageView.setImageDrawable(blurhashBitmap);
} else {
imageView.setImageDrawable(new ColorDrawable(ThemeUtils.getColor(
context, R.attr.sensitive_media_warning_background_color)));
}
}
final Attachment.Type type = attachment.getType();
if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) {
mediaOverlays[i].setVisibility(View.VISIBLE);
} else {
mediaOverlays[i].setVisibility(View.GONE);
}
setAttachmentClickListener(imageView, listener, i, attachment, true);
}
final String hiddenContentText;
if (sensitive) {
hiddenContentText = context.getString(R.string.status_sensitive_media_template,
context.getString(R.string.status_sensitive_media_title),
context.getString(R.string.status_sensitive_media_directions));
hiddenContentText = context.getString(R.string.status_sensitive_media_title);
} else {
hiddenContentText = context.getString(R.string.status_sensitive_media_template,
context.getString(R.string.status_media_hidden_title),
context.getString(R.string.status_sensitive_media_directions));
hiddenContentText = context.getString(R.string.status_media_hidden_title);
}
sensitiveMediaWarning.setText(HtmlUtils.fromHtml(hiddenContentText));
@ -621,7 +647,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
view.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onViewMedia(position, index, animateTransition ? v : null);
if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) {
listener.onContentHiddenChange(true, getAdapterPosition());
} else {
listener.onViewMedia(position, index, animateTransition ? v : null);
}
}
});
view.setOnLongClickListener(v -> {
@ -633,7 +663,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private static CharSequence getAttachmentDescription(Context context, Attachment attachment) {
String duration = "";
if(attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) {
if (attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) {
duration = formatDuration(attachment.getMeta().getDuration()) + " ";
}
if (TextUtils.isEmpty(attachment.getDescription())) {
@ -733,31 +763,29 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar) {
this.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, null);
StatusDisplayOptions statusDisplayOptions) {
this.setupWithStatus(status, listener, statusDisplayOptions, null);
}
protected void setupWithStatus(StatusViewData.Concrete status,
final StatusActionListener listener,
boolean mediaPreviewEnabled,
boolean showBotOverlay,
boolean animateAvatar,
StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
if (payloads == null) {
setDisplayName(status.getUserFullName(), status.getAccountEmojis());
setUsername(status.getNickname());
setCreatedAt(status.getCreatedAt());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setStatusVisibility(status.getVisibility());
setIsReply(status.getInReplyToId() != null);
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), showBotOverlay, animateAvatar);
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions);
setReblogged(status.isReblogged());
setFavourited(status.isFavourited());
setQuoteContainer(status.getQuote(), listener);
setBookmarked(status.isBookmarked());
List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.isSensitive();
if (mediaPreviewEnabled && !hasAudioAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent());
if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
if (attachments.size() == 0) {
hideSensitiveMediaWarning();
@ -780,9 +808,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setRebloggingEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
setQuoteEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), listener, status.getQuote() != null);
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener, status.getQuote() != null);
setDescriptionForStatus(status);
setDescriptionForStatus(status, statusDisplayOptions);
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
// RecyclerView tries to set AccessibilityDelegateCompat to null
@ -794,7 +822,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (payloads instanceof List)
for (Object item : (List) payloads) {
if (Key.KEY_CREATED.equals(item)) {
setCreatedAt(status.getCreatedAt());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
}
}
@ -802,7 +830,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
protected static boolean hasAudioAttachment(List<Attachment> attachments) {
for(Attachment attachment: attachments) {
for (Attachment attachment : attachments) {
if (attachment.getType() == Attachment.Type.AUDIO) {
return true;
}
@ -810,14 +838,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
return false;
}
private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status) {
private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status,
StatusDisplayOptions statusDisplayOptions) {
Context context = itemView.getContext();
String description = context.getString(R.string.description_status,
status.getUserFullName(),
getContentWarningDescription(context, status),
(TextUtils.isEmpty(status.getSpoilerText()) || !status.isSensitive() || status.isExpanded() ? status.getContent() : ""),
getCreatedAtDescription(status.getCreatedAt()),
getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions),
getReblogDescription(context, status),
status.getNickname(),
status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "",
@ -827,7 +856,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
getVisibilityDescription(context, status.getVisibility()),
getFavsText(context, status.getFavouritesCount()),
getReblogsText(context, status.getReblogsCount()),
getPollDescription(context, status)
getPollDescription(status, context, statusDisplayOptions)
);
itemView.setContentDescription(description);
}
@ -875,7 +904,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private static CharSequence getVisibilityDescription(Context context, Status.Visibility visibility) {
if(visibility == null) {
if (visibility == null) {
return "";
}
@ -899,8 +928,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
return context.getString(resource);
}
private CharSequence getPollDescription(Context context,
@NonNull StatusViewData.Concrete status) {
private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status,
Context context,
StatusDisplayOptions statusDisplayOptions) {
PollViewData poll = status.getPoll();
if (poll == null) {
return "";
@ -915,7 +945,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
args[i] = "";
}
}
args[4] = getPollInfoText(System.currentTimeMillis(), poll, context);
args[4] = getPollInfoText(System.currentTimeMillis(), poll, statusDisplayOptions,
context);
return context.getString(R.string.description_poll, args);
}
}
@ -938,7 +969,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
private void setupPoll(PollViewData poll, List<Emoji> emojis, StatusActionListener listener) {
private void setupPoll(PollViewData poll, List<Emoji> emojis,
StatusDisplayOptions statusDisplayOptions,
StatusActionListener listener) {
long timestamp = System.currentTimeMillis();
boolean expired = poll.getExpired() || (poll.getExpiresAt() != null && timestamp > poll.getExpiresAt().getTime());
@ -975,10 +1008,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
pollDescription.setVisibility(View.VISIBLE);
pollDescription.setText(getPollInfoText(timestamp, poll, context));
pollDescription.setText(getPollInfoText(timestamp, poll, statusDisplayOptions, context));
}
private CharSequence getPollInfoText(long timestamp, PollViewData poll, Context context) {
private CharSequence getPollInfoText(long timestamp, PollViewData poll,
StatusDisplayOptions statusDisplayOptions,
Context context) {
String votes = numberFormat.format(poll.getVotesCount());
String votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), votes);
CharSequence pollDurationInfo;
@ -987,7 +1022,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} else if (poll.getExpiresAt() == null) {
return votesText;
} else {
if (useAbsoluteTime) {
if (statusDisplayOptions.useAbsoluteTime()) {
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
} else {
String pollDuration = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);

View File

@ -4,11 +4,9 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
@ -27,8 +25,8 @@ import com.keylesspalace.tusky.ViewThreadActivity;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomURLSpan;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.DateFormat;
@ -47,8 +45,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
private TextView cardUrl;
private View infoDivider;
StatusDetailedViewHolder(View view, boolean useAbsoluteTime) {
super(view, useAbsoluteTime);
StatusDetailedViewHolder(View view) {
super(view);
reblogs = view.findViewById(R.id.status_reblogs);
favourites = view.findViewById(R.id.status_favourites);
cardView = view.findViewById(R.id.card_view);
@ -68,8 +66,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
@Override
protected void setCreatedAt(Date createdAt) {
if(createdAt == null) {
protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
if (createdAt == null) {
timestampInfo.setText("");
} else {
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT);
@ -128,10 +126,11 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
@Override
protected void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar,
protected void setupWithStatus(final StatusViewData.Concrete status,
final StatusActionListener listener,
StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloads);
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
@ -155,11 +154,11 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
final Card card = status.getCard();
cardView.setVisibility(View.VISIBLE);
cardTitle.setText(card.getTitle());
if(TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) {
if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) {
cardDescription.setVisibility(View.GONE);
} else {
cardDescription.setVisibility(View.VISIBLE);
if(TextUtils.isEmpty(card.getDescription())) {
if (TextUtils.isEmpty(card.getDescription())) {
cardDescription.setText(card.getAuthorName());
} else {
cardDescription.setText(card.getDescription());
@ -200,7 +199,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
Glide.with(cardImage)
.load(card.getImage())
.transform(

View File

@ -22,14 +22,15 @@ import android.view.View;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import at.connyduck.sparkbutton.helpers.Utils;
public class StatusViewHolder extends StatusBaseViewHolder {
@ -39,8 +40,8 @@ public class StatusViewHolder extends StatusBaseViewHolder {
private TextView statusInfo;
private ToggleButton contentCollapseButton;
public StatusViewHolder(View itemView, boolean useAbsoluteTime) {
super(itemView, useAbsoluteTime);
public StatusViewHolder(View itemView) {
super(itemView);
statusInfo = itemView.findViewById(R.id.status_info);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
}
@ -51,8 +52,9 @@ public class StatusViewHolder extends StatusBaseViewHolder {
}
@Override
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
boolean mediaPreviewEnabled, boolean showBotOverlay, boolean animateAvatar,
protected void setupWithStatus(StatusViewData.Concrete status,
final StatusActionListener listener,
StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) {
if (payloads == null) {
@ -67,7 +69,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
}
}
super.setupWithStatus(status, listener, mediaPreviewEnabled, showBotOverlay, animateAvatar, payloads);
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
}

View File

@ -15,15 +15,17 @@
package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.ArrayList;
@ -34,20 +36,14 @@ public class ThreadAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_STATUS_DETAILED = 1;
private List<StatusViewData.Concrete> statuses;
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusActionListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
private int detailedStatusPosition;
public ThreadAdapter(StatusActionListener listener) {
public ThreadAdapter(StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) {
this.statusDisplayOptions = statusDisplayOptions;
this.statusActionListener = listener;
this.statuses = new ArrayList<>();
mediaPreviewEnabled = true;
useAbsoluteTime = false;
showBotOverlay = true;
animateAvatar = false;
detailedStatusPosition = RecyclerView.NO_POSITION;
}
@ -59,12 +55,12 @@ public class ThreadAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_STATUS: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_status, parent, false);
return new StatusViewHolder(view, useAbsoluteTime);
return new StatusViewHolder(view);
}
case VIEW_TYPE_STATUS_DETAILED: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_status_detailed, parent, false);
return new StatusDetailedViewHolder(view, useAbsoluteTime);
return new StatusDetailedViewHolder(view);
}
}
}
@ -74,10 +70,10 @@ public class ThreadAdapter extends RecyclerView.Adapter {
StatusViewData.Concrete status = statuses.get(position);
if (position == detailedStatusPosition) {
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled, showBotOverlay, animateAvatar);
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
} else {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus(status, statusActionListener, mediaPreviewEnabled, showBotOverlay, animateAvatar);
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
}
}
@ -151,22 +147,6 @@ public class ThreadAdapter extends RecyclerView.Adapter {
}
}
public void setMediaPreviewEnabled(boolean enabled) {
mediaPreviewEnabled = enabled;
}
public void setUseAbsoluteTime(boolean useAbsoluteTime) {
this.useAbsoluteTime = useAbsoluteTime;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
}
public void setDetailedStatusPosition(int position) {
if (position != detailedStatusPosition
&& detailedStatusPosition != RecyclerView.NO_POSITION) {

View File

@ -15,19 +15,21 @@
package com.keylesspalace.tusky.adapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.util.List;
public final class TimelineAdapter extends RecyclerView.Adapter {
public interface AdapterDataSource<T> {
@ -40,20 +42,29 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_PLACEHOLDER = 2;
private final AdapterDataSource<StatusViewData> dataSource;
private StatusDisplayOptions statusDisplayOptions;
private final StatusActionListener statusListener;
private boolean mediaPreviewEnabled;
private boolean useAbsoluteTime;
private boolean showBotOverlay;
private boolean animateAvatar;
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
StatusDisplayOptions statusDisplayOptions,
StatusActionListener statusListener) {
this.dataSource = dataSource;
this.statusDisplayOptions = statusDisplayOptions;
this.statusListener = statusListener;
mediaPreviewEnabled = true;
useAbsoluteTime = false;
showBotOverlay = true;
animateAvatar = false;
}
public boolean getMediaPreviewEnabled() {
return statusDisplayOptions.mediaPreviewEnabled();
}
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
this.statusDisplayOptions = statusDisplayOptions.copy(
statusDisplayOptions.animateAvatars(),
mediaPreviewEnabled,
statusDisplayOptions.useAbsoluteTime(),
statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash()
);
}
@NonNull
@ -64,7 +75,7 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_STATUS: {
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_status, viewGroup, false);
return new StatusViewHolder(view, useAbsoluteTime);
return new StatusViewHolder(view);
}
case VIEW_TYPE_PLACEHOLDER: {
View view = LayoutInflater.from(viewGroup.getContext())
@ -76,16 +87,16 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
bindViewHolder(viewHolder,position,null);
bindViewHolder(viewHolder, position, null);
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) {
bindViewHolder(viewHolder,position,payloads);
bindViewHolder(viewHolder, position, payloads);
}
private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads){
private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) {
StatusViewData status = dataSource.getItemAt(position);
if (status instanceof StatusViewData.Placeholder) {
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
@ -94,12 +105,11 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
StatusViewHolder holder = (StatusViewHolder) viewHolder;
holder.setupWithStatus((StatusViewData.Concrete) status,
statusListener,
mediaPreviewEnabled,
showBotOverlay,
animateAvatar,
statusDisplayOptions,
payloads != null && !payloads.isEmpty() ? payloads.get(0) : null);
}
}
@Override
public int getItemCount() {
return dataSource.getItemCount();
@ -114,26 +124,6 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
}
}
public void setMediaPreviewEnabled(boolean enabled) {
mediaPreviewEnabled = enabled;
}
public void setUseAbsoluteTime(boolean useAbsoluteTime){
this.useAbsoluteTime = useAbsoluteTime;
}
public boolean getMediaPreviewEnabled() {
return mediaPreviewEnabled;
}
public void setShowBotOverlay(boolean showBotOverlay) {
this.showBotOverlay = showBotOverlay;
}
public void setAnimateAvatar(boolean animateAvatar) {
this.animateAvatar = animateAvatar;
}
@Override
public long getItemId(int position) {
return dataSource.getItemAt(position).getViewDataId();

View File

@ -21,38 +21,23 @@ import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.search.SearchType
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.InstanceEntity
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.service.ServiceClient
import com.keylesspalace.tusky.service.TootToSend
import com.keylesspalace.tusky.util.*
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import io.reactivex.rxkotlin.Singles
import java.util.*
import javax.inject.Inject
open class RxAwareViewModel : ViewModel() {
private val disposables = CompositeDisposable()
fun Disposable.autoDispose() = disposables.add(this)
override fun onCleared() {
super.onCleared()
disposables.clear()
}
}
/**
* Throw when trying to add an image when video is already present or the other way around
*/

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
@ -80,14 +77,15 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setDisplayName(account.getDisplayName(), account.getEmojis());
setUsername(account.getUsername());
setCreatedAt(status.getCreatedAt());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null);
setFavourited(status.getFavourited());
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();
@ -108,7 +106,9 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setupButtons(listener, account.getId(), false, account.getUsername());
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getEmojis(), PollViewDataKt.toViewData(status.getPoll()), listener, false);
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
status.getMentions(), status.getEmojis(),
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener, false);
setConversationName(conversation.getAccounts());
@ -118,11 +118,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 +130,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

@ -4,15 +4,13 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.paging.PagedList
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.util.Listing
import com.keylesspalace.tusky.util.NetworkState
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import com.keylesspalace.tusky.util.RxAwareViewModel
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
@ -21,7 +19,7 @@ class ConversationsViewModel @Inject constructor(
private val timelineCases: TimelineCases,
private val database: AppDatabase,
private val accountManager: AccountManager
) : ViewModel() {
) : RxAwareViewModel() {
private val repoResult = MutableLiveData<Listing<ConversationEntity>>()
@ -29,8 +27,6 @@ class ConversationsViewModel @Inject constructor(
val networkState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkState }
val refreshState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
private val disposables = CompositeDisposable()
fun load() {
val accountId = accountManager.activeAccount?.id ?: return
if (repoResult.value == null) {
@ -61,7 +57,7 @@ class ConversationsViewModel @Inject constructor(
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
.onErrorReturnItem(0)
.subscribe()
.addTo(disposables)
.autoDispose()
}
}
@ -79,7 +75,7 @@ class ConversationsViewModel @Inject constructor(
.subscribeOn(Schedulers.io())
.doOnError { t -> Log.w("ConversationViewModel", "Failed to bookmark conversation", t) }
.subscribe()
.addTo(disposables)
.autoDispose()
}
}
@ -98,7 +94,7 @@ class ConversationsViewModel @Inject constructor(
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
.onErrorReturnItem(0)
.subscribe()
.addTo(disposables)
.autoDispose()
}
}
@ -150,8 +146,4 @@ class ConversationsViewModel @Inject constructor(
.subscribe()
}
override fun onCleared() {
disposables.dispose()
}
}

View File

@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.report
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.paging.PagedList
import com.keylesspalace.tusky.components.report.adapter.StatusesRepository
import com.keylesspalace.tusky.components.report.model.StatusViewState
@ -26,16 +25,13 @@ import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.*
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
class ReportViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
private val statusesRepository: StatusesRepository) : ViewModel() {
private val disposables = CompositeDisposable()
private val statusesRepository: StatusesRepository) : RxAwareViewModel() {
private val navigationMutable = MutableLiveData<Screen>()
val navigation: LiveData<Screen> = navigationMutable
@ -87,11 +83,6 @@ class ReportViewModel @Inject constructor(
repoResult.value = statusesRepository.getStatuses(accountId, statusId, disposables)
}
override fun onCleared() {
super.onCleared()
disposables.clear()
}
fun navigateTo(screen: Screen) {
navigationMutable.value = screen
}
@ -105,19 +96,19 @@ class ReportViewModel @Inject constructor(
val ids = listOf(accountId)
muteStateMutable.value = Loading()
blockStateMutable.value = Loading()
disposables.add(
mastodonApi.relationshipsObservable(ids)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ data ->
updateRelationship(data.getOrNull(0))
mastodonApi.relationshipsObservable(ids)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ data ->
updateRelationship(data.getOrNull(0))
},
{
updateRelationship(null)
}
))
},
{
updateRelationship(null)
}
)
.autoDispose()
}
@ -132,62 +123,61 @@ class ReportViewModel @Inject constructor(
}
fun toggleMute() {
val single: Single<Relationship> = if (muteStateMutable.value?.data == true) {
if (muteStateMutable.value?.data == true) {
mastodonApi.unmuteAccountObservable(accountId)
} else {
mastodonApi.muteAccountObservable(accountId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
muteStateMutable.value = Success(relationship?.muting == true)
},
{ error ->
muteStateMutable.value = Error(false, error.message)
}
).autoDispose()
muteStateMutable.value = Loading()
disposables.add(
single
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
muteStateMutable.value = Success(relationship?.muting == true)
},
{ error ->
muteStateMutable.value = Error(false, error.message)
}
))
}
fun toggleBlock() {
val single: Single<Relationship> = if (blockStateMutable.value?.data == true) {
if (blockStateMutable.value?.data == true) {
mastodonApi.unblockAccountObservable(accountId)
} else {
mastodonApi.blockAccountObservable(accountId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
blockStateMutable.value = Success(relationship?.blocking == true)
},
{ error ->
blockStateMutable.value = Error(false, error.message)
}
)
.autoDispose()
blockStateMutable.value = Loading()
disposables.add(
single
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
blockStateMutable.value = Success(relationship?.blocking == true)
},
{ error ->
blockStateMutable.value = Error(false, error.message)
}
))
}
fun doReport() {
reportingStateMutable.value = Loading()
disposables.add(
mastodonApi.reportObservable(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
reportingStateMutable.value = Success(true)
},
{ error ->
reportingStateMutable.value = Error(cause = error)
}
)
)
mastodonApi.reportObservable(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
reportingStateMutable.value = Success(true)
},
{ error ->
reportingStateMutable.value = Error(cause = error)
}
)
.autoDispose()
}
fun retryStatusLoad() {

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)
@ -60,6 +61,7 @@ class StatusViewHolder(itemView: View,
adapterHandler.setStatusChecked(status, isChecked)
}
}
itemView.status_media_preview_container.clipToOutline = true
}
fun bind(status: Status) {
@ -69,11 +71,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)
}
@ -128,7 +130,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

@ -0,0 +1,150 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.scheduled
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.util.Status
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.activity_scheduled_toot.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject
class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable {
@Inject
lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: ScheduledTootViewModel
private val adapter = ScheduledTootAdapter(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scheduled_toot)
setSupportActionBar(toolbar)
supportActionBar?.run {
title = getString(R.string.title_scheduled_toot)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses)
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(
ThemeUtils.getColor(this, android.R.attr.colorBackground))
scheduledTootList.setHasFixedSize(true)
scheduledTootList.layoutManager = LinearLayoutManager(this)
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
scheduledTootList.addItemDecoration(divider)
scheduledTootList.adapter = adapter
viewModel = ViewModelProvider(this, viewModelFactory)[ScheduledTootViewModel::class.java]
viewModel.data.observe(this, Observer {
adapter.submitList(it)
})
viewModel.networkState.observe(this, Observer { (status) ->
when(status) {
Status.SUCCESS -> {
progressBar.hide()
swipeRefreshLayout.isRefreshing = false
if(viewModel.data.value?.loadedCount == 0) {
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status)
errorMessageView.show()
} else {
errorMessageView.hide()
}
}
Status.RUNNING -> {
errorMessageView.hide()
if(viewModel.data.value?.loadedCount ?: 0 > 0) {
swipeRefreshLayout.isRefreshing = true
} else {
progressBar.show()
}
}
Status.FAILED -> {
if(viewModel.data.value?.loadedCount ?: 0 >= 0) {
progressBar.hide()
swipeRefreshLayout.isRefreshing = false
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) {
refreshStatuses()
}
errorMessageView.show()
}
}
}
})
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun refreshStatuses() {
viewModel.reload()
}
override fun edit(item: ScheduledStatus) {
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
tootText = item.params.text,
contentWarning = item.params.spoilerText,
mediaAttachments = item.mediaAttachments,
inReplyToId = item.params.inReplyToId,
visibility = item.params.visibility,
scheduledAt = item.scheduledAt,
sensitive = item.params.sensitive
))
startActivity(intent)
}
override fun delete(item: ScheduledStatus) {
viewModel.deleteScheduledStatus(item)
}
companion object {
@JvmStatic
fun newIntent(context: Context): Intent {
return Intent(context, ScheduledTootActivity::class.java)
}
}
}

View File

@ -0,0 +1,85 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.scheduled
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.ScheduledStatus
interface ScheduledTootActionListener {
fun edit(item: ScheduledStatus)
fun delete(item: ScheduledStatus)
}
class ScheduledTootAdapter(
val listener: ScheduledTootActionListener
) : PagedListAdapter<ScheduledStatus, ScheduledTootAdapter.TootViewHolder>(
object: DiffUtil.ItemCallback<ScheduledStatus>(){
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TootViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_scheduled_toot, parent, false)
return TootViewHolder(view)
}
override fun onBindViewHolder(viewHolder: TootViewHolder, position: Int) {
getItem(position)?.let{
viewHolder.bind(it)
}
}
inner class TootViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val text: TextView = view.findViewById(R.id.text)
private val edit: ImageButton = view.findViewById(R.id.edit)
private val delete: ImageButton = view.findViewById(R.id.delete)
fun bind(item: ScheduledStatus) {
edit.isEnabled = true
delete.isEnabled = true
text.text = item.params.text
edit.setOnClickListener { v: View ->
v.isEnabled = false
listener.edit(item)
}
delete.setOnClickListener { v: View ->
v.isEnabled = false
listener.delete(item)
}
}
}
}

View File

@ -0,0 +1,102 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.scheduled
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.paging.DataSource
import androidx.paging.ItemKeyedDataSource
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.NetworkState
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
class ScheduledTootDataSourceFactory(
private val mastodonApi: MastodonApi,
private val disposables: CompositeDisposable
): DataSource.Factory<String, ScheduledStatus>() {
private val scheduledTootsCache = mutableListOf<ScheduledStatus>()
private var dataSource: ScheduledTootDataSource? = null
val networkState = MutableLiveData<NetworkState>()
override fun create(): DataSource<String, ScheduledStatus> {
return ScheduledTootDataSource(mastodonApi, disposables, scheduledTootsCache, networkState).also {
dataSource = it
}
}
fun reload() {
scheduledTootsCache.clear()
dataSource?.invalidate()
}
fun remove(status: ScheduledStatus) {
scheduledTootsCache.remove(status)
dataSource?.invalidate()
}
}
class ScheduledTootDataSource(
private val mastodonApi: MastodonApi,
private val disposables: CompositeDisposable,
private val scheduledTootsCache: MutableList<ScheduledStatus>,
private val networkState: MutableLiveData<NetworkState>
): ItemKeyedDataSource<String, ScheduledStatus>() {
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<ScheduledStatus>) {
if(scheduledTootsCache.isNotEmpty()) {
callback.onResult(scheduledTootsCache.toList())
} else {
networkState.postValue(NetworkState.LOADING)
mastodonApi.scheduledStatuses(limit = params.requestedLoadSize)
.subscribe({ newData ->
scheduledTootsCache.addAll(newData)
callback.onResult(newData)
networkState.postValue(NetworkState.LOADED)
}, { throwable ->
Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable)
networkState.postValue(NetworkState.error(throwable.message))
})
.addTo(disposables)
}
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<ScheduledStatus>) {
mastodonApi.scheduledStatuses(limit = params.requestedLoadSize, maxId = params.key)
.subscribe({ newData ->
scheduledTootsCache.addAll(newData)
callback.onResult(newData)
}, { throwable ->
Log.w("ScheduledTootDataSource", "Error loading scheduled statuses", throwable)
networkState.postValue(NetworkState.error(throwable.message))
})
.addTo(disposables)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<ScheduledStatus>) {
// we are always loading from beginning to end
}
override fun getKey(item: ScheduledStatus): String {
return item.id
}
}

View File

@ -0,0 +1,68 @@
/* Copyright 2019 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.scheduled
import android.util.Log
import androidx.paging.Config
import androidx.paging.toLiveData
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.RxAwareViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
import javax.inject.Inject
class ScheduledTootViewModel @Inject constructor(
val mastodonApi: MastodonApi,
val eventHub: EventHub
): RxAwareViewModel() {
private val dataSourceFactory = ScheduledTootDataSourceFactory(mastodonApi, disposables)
val data = dataSourceFactory.toLiveData(
config = Config(pageSize = 20, initialLoadSizeHint = 20, enablePlaceholders = false)
)
val networkState = dataSourceFactory.networkState
init {
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
.subscribe { event ->
if (event is StatusScheduledEvent) {
reload()
}
}
.autoDispose()
}
fun reload() {
dataSourceFactory.reload()
}
fun deleteScheduledStatus(status: ScheduledStatus) {
mastodonApi.deleteScheduledStatus(status.id)
.subscribe({
dataSourceFactory.remove(status)
},{ throwable ->
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
})
.autoDispose()
}
}

View File

@ -4,7 +4,6 @@ import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.paging.PagedList
import com.keylesspalace.tusky.components.search.adapter.SearchNotestockRepository
import com.keylesspalace.tusky.components.search.adapter.SearchRepository
@ -16,19 +15,18 @@ import com.keylesspalace.tusky.network.NotestockApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.util.Listing
import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.ViewDataUtils
import com.keylesspalace.tusky.viewdata.StatusViewData
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import javax.inject.Inject
class SearchViewModel @Inject constructor(
mastodonApi: MastodonApi,
notestockApi: NotestockApi,
private val timelineCases: TimelineCases,
private val accountManager: AccountManager) : ViewModel() {
private val accountManager: AccountManager) : RxAwareViewModel() {
var currentQuery: String = ""
@ -40,7 +38,6 @@ class SearchViewModel @Inject constructor(
val mediaPreviewEnabled: Boolean
get() = activeAccount?.mediaPreviewEnabled ?: false
private val disposables = CompositeDisposable()
private val statusesRepository = SearchRepository<Pair<Status, StatusViewData.Concrete>>(mastodonApi)
private val accountsRepository = SearchRepository<Account>(mastodonApi)
@ -101,11 +98,6 @@ class SearchViewModel @Inject constructor(
}
override fun onCleared() {
super.onCleared()
disposables.clear()
}
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
timelineCases.delete(status.first.id)
.subscribe({
@ -114,7 +106,7 @@ class SearchViewModel @Inject constructor(
}, {
err -> Log.d(TAG, "Failed to delete status", err)
})
.addTo(disposables)
.autoDispose()
}
@ -142,13 +134,13 @@ class SearchViewModel @Inject constructor(
}
fun reblog(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
disposables.add(timelineCases.reblog(status.first, reblog)
timelineCases.reblog(status.first, reblog)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ setRebloggedForStatus(status, reblog) },
{ err -> Log.d(TAG, "Failed to reblog status ${status.first.id}", err) }
)
)
.autoDispose()
}
private fun setRebloggedForStatus(status: Pair<Status, StatusViewData.Concrete>, reblog: Boolean) {
@ -202,7 +194,7 @@ class SearchViewModel @Inject constructor(
fun voteInPoll(status: Pair<Status, StatusViewData.Concrete>, choices: MutableList<Int>) {
val votedPoll = status.first.actionableStatus.poll!!.votedCopy(choices)
updateStatus(status, votedPoll)
disposables.add(timelineCases.voteInPoll(status.first, choices)
timelineCases.voteInPoll(status.first, choices)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ newPoll -> updateStatus(status, newPoll) },
@ -210,7 +202,8 @@ class SearchViewModel @Inject constructor(
Log.d(TAG,
"Failed to vote in poll: ${status.first.id}", t)
}
))
)
.autoDispose()
}
private fun updateStatus(status: Pair<Status, StatusViewData.Concrete>, newPoll: Poll) {
@ -232,9 +225,10 @@ class SearchViewModel @Inject constructor(
loadedStatuses[idx] = newPair
repoResultStatus.value?.refresh?.invoke()
}
disposables.add(timelineCases.favourite(status.first, isFavorited)
timelineCases.favourite(status.first, isFavorited)
.onErrorReturnItem(status.first)
.subscribe())
.subscribe()
.autoDispose()
}
fun bookmark(status: Pair<Status, StatusViewData.Concrete>, isBookmarked: Boolean) {
@ -244,9 +238,10 @@ class SearchViewModel @Inject constructor(
loadedStatuses[idx] = newPair
repoResultStatus.value?.refresh?.invoke()
}
disposables.add(timelineCases.favourite(status.first, isBookmarked)
timelineCases.favourite(status.first, isBookmarked)
.onErrorReturnItem(status.first)
.subscribe())
.subscribe()
.autoDispose()
}
fun getAllAccountsOrderedByActive(): List<AccountEntity> {

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 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
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)
}

View File

@ -19,6 +19,7 @@ import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity
import dagger.Module
import dagger.android.ContributesAndroidInjector

View File

@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
import com.keylesspalace.tusky.components.search.SearchViewModel
import com.keylesspalace.tusky.viewmodel.AccountViewModel
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
@ -79,5 +80,10 @@ abstract class ViewModelModule {
@ViewModelKey(ComposeViewModel::class)
internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(ScheduledTootViewModel::class)
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
//Add more ViewModels here
}

View File

@ -31,7 +31,8 @@ data class Attachment(
@SerializedName("preview_url") val previewUrl: String,
val meta: MetaData?,
val type: Type,
val description: String?
val description: String?,
val blurhash: String?
) : Parcelable {
@JsonAdapter(MediaTypeDeserializer::class)

View File

@ -75,6 +75,7 @@ import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.NotificationTypeConverterKt;
import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.BackgroundMessageView;
@ -237,18 +238,18 @@ public class NotificationsFragment extends SFragment implements
recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false),
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true)
);
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
dataSource, this, this);
dataSource, statusDisplayOptions, this, this);
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
adapter.setUseAbsoluteTime(useAbsoluteTime);
boolean showBotOverlay = preferences.getBoolean("showBotOverlay", true);
adapter.setShowBotOverlay(showBotOverlay);
boolean animateAvatar = preferences.getBoolean("animateGifAvatars", false);
adapter.setAnimateAvatar(animateAvatar);
recyclerView.setAdapter(adapter);
topLoading = false;

View File

@ -83,6 +83,7 @@ import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils;
@ -238,11 +239,17 @@ public class TimelineFragment extends SFragment implements
hashtagOrId = arguments.getString(HASHTAG_OR_ID_ARG);
}
adapter = new TimelineAdapter(dataSource, this);
preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false),
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true)
);
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true);
preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
}
@Override
@ -411,18 +418,10 @@ public class TimelineFragment extends SFragment implements
}
private void setupTimelinePreferences() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
adapter.setUseAbsoluteTime(useAbsoluteTime);
boolean showBotOverlay = preferences.getBoolean("showBotOverlay", true);
adapter.setShowBotOverlay(showBotOverlay);
boolean animateAvatar = preferences.getBoolean("animateGifAvatars", false);
adapter.setAnimateAvatar(animateAvatar);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
boolean filter = preferences.getBoolean("tabFilterHomeReplies", true);
filterRemoveReplies = kind == Kind.HOME && !filter;

View File

@ -60,6 +60,7 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
@ -123,8 +124,16 @@ public final class ViewThreadFragment extends SFragment implements
super.onCreate(savedInstanceState);
thisThreadsStatusId = getArguments().getString("id");
adapter = new ThreadAdapter(this);
SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(getActivity());
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false),
accountManager.getActiveAccount().getMediaPreviewEnabled(),
preferences.getBoolean("absoluteTimeView", false),
preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true)
);
adapter = new ThreadAdapter(statusDisplayOptions, this);
}
@Override
@ -150,18 +159,8 @@ public final class ViewThreadFragment extends SFragment implements
recyclerView.addItemDecoration(divider);
recyclerView.addItemDecoration(new ConversationLineItemDecoration(context));
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
getActivity());
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
adapter.setUseAbsoluteTime(useAbsoluteTime);
boolean animateAvatars = preferences.getBoolean("animateGifAvatars", false);
adapter.setAnimateAvatar(animateAvatars);
boolean showBotIndicator = preferences.getBoolean("showBotOverlay", true);
adapter.setShowBotOverlay(showBotIndicator);
reloadFilters(false);
recyclerView.setAdapter(adapter);

View File

@ -201,12 +201,15 @@ interface MastodonApi {
): Single<Status>
@GET("api/v1/scheduled_statuses")
fun scheduledStatuses(): Call<List<ScheduledStatus>>
fun scheduledStatuses(
@Query("limit") limit: Int? = null,
@Query("max_id") maxId: String? = null
): Single<List<ScheduledStatus>>
@DELETE("api/v1/scheduled_statuses/{id}")
fun deleteScheduledStatus(
@Path("id") scheduledStatusId: String
): Call<ResponseBody>
): Single<ResponseBody>
@GET("api/v1/accounts/verify_credentials")
fun accountVerifyCredentials(): Single<Account>

View File

@ -0,0 +1,130 @@
/**
* Blurhash implementation from blurhash project:
* https://github.com/woltapp/blurhash
* Minor modifications by charlag
*/
package com.keylesspalace.tusky.util
import android.graphics.Bitmap
import android.graphics.Color
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.withSign
object BlurHashDecoder {
fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? {
require(width > 0) { "Width must be greater than zero" }
require(height > 0) { "height must be greater than zero" }
if (blurHash == null || blurHash.length < 6) {
return null
}
val numCompEnc = decode83(blurHash, 0, 1)
val numCompX = (numCompEnc % 9) + 1
val numCompY = (numCompEnc / 9) + 1
if (blurHash.length != 4 + 2 * numCompX * numCompY) {
return null
}
val maxAcEnc = decode83(blurHash, 1, 2)
val maxAc = (maxAcEnc + 1) / 166f
val colors = Array(numCompX * numCompY) { i ->
if (i == 0) {
val colorEnc = decode83(blurHash, 2, 6)
decodeDc(colorEnc)
} else {
val from = 4 + i * 2
val colorEnc = decode83(blurHash, from, from + 2)
decodeAc(colorEnc, maxAc * punch)
}
}
return composeBitmap(width, height, numCompX, numCompY, colors)
}
private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int {
var result = 0
for (i in from until to) {
val index = charMap[str[i]] ?: -1
if (index != -1) {
result = result * 83 + index
}
}
return result
}
private fun decodeDc(colorEnc: Int): FloatArray {
val r = colorEnc shr 16
val g = (colorEnc shr 8) and 255
val b = colorEnc and 255
return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b))
}
private fun srgbToLinear(colorEnc: Int): Float {
val v = colorEnc / 255f
return if (v <= 0.04045f) {
(v / 12.92f)
} else {
((v + 0.055f) / 1.055f).pow(2.4f)
}
}
private fun decodeAc(value: Int, maxAc: Float): FloatArray {
val r = value / (19 * 19)
val g = (value / 19) % 19
val b = value % 19
return floatArrayOf(
signedPow2((r - 9) / 9.0f) * maxAc,
signedPow2((g - 9) / 9.0f) * maxAc,
signedPow2((b - 9) / 9.0f) * maxAc
)
}
private fun signedPow2(value: Float) = value.pow(2f).withSign(value)
private fun composeBitmap(
width: Int, height: Int,
numCompX: Int, numCompY: Int,
colors: Array<FloatArray>
): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
for (y in 0 until height) {
for (x in 0 until width) {
var r = 0f
var g = 0f
var b = 0f
for (j in 0 until numCompY) {
for (i in 0 until numCompX) {
val basis = (cos(PI * x * i / width) * cos(PI * y * j / height)).toFloat()
val color = colors[j * numCompX + i]
r += color[0] * basis
g += color[1] * basis
b += color[2] * basis
}
}
bitmap.setPixel(x, y, Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)))
}
}
return bitmap
}
private fun linearToSrgb(value: Float): Int {
val v = value.coerceIn(0f, 1f)
return if (v <= 0.0031308f) {
(v * 12.92f * 255f + 0.5f).toInt()
} else {
((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt()
}
}
private val charMap = listOf(
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',',
'-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
)
.mapIndexed { i, c -> c to i }
.toMap()
}

View File

@ -2,6 +2,8 @@
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.widget.ImageView
import androidx.annotation.Px
import com.bumptech.glide.Glide
@ -14,7 +16,7 @@ private val centerCropTransformation = CenterCrop()
fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boolean) {
if(url.isNullOrBlank()) {
if (url.isNullOrBlank()) {
Glide.with(imageView)
.load(R.drawable.avatar_default)
.into(imageView)
@ -42,4 +44,8 @@ fun loadAvatar(url: String?, imageView: ImageView, @Px radius: Int, animate: Boo
}
}
}
fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable {
return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f))
}

View File

@ -0,0 +1,16 @@
package com.keylesspalace.tusky.util
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
open class RxAwareViewModel : ViewModel() {
val disposables = CompositeDisposable()
fun Disposable.autoDispose() = disposables.add(this)
override fun onCleared() {
super.onCleared()
disposables.clear()
}
}

View File

@ -0,0 +1,14 @@
package com.keylesspalace.tusky.util
data class StatusDisplayOptions(
@get:JvmName("animateAvatars")
val animateAvatars: Boolean,
@get:JvmName("mediaPreviewEnabled")
val mediaPreviewEnabled: Boolean,
@get:JvmName("useAbsoluteTime")
val useAbsoluteTime: Boolean,
@get:JvmName("showBotOverlay")
val showBotOverlay: Boolean,
@get:JvmName("useBlurhash")
val useBlurhash: Boolean
)

View File

@ -16,6 +16,7 @@
package com.keylesspalace.tusky.util
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.text.InputFilter
import android.text.TextUtils
import android.view.View
@ -47,7 +48,7 @@ class StatusViewHelper(private val itemView: View) {
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
fun setMediasPreview(
mediaPreviewEnabled: Boolean,
statusDisplayOptions: StatusDisplayOptions,
attachments: List<Attachment>,
sensitive: Boolean,
previewListener: MediaPreviewListener,
@ -70,7 +71,7 @@ class StatusViewHelper(private val itemView: View) {
val sensitiveMediaWarning = itemView.findViewById<TextView>(R.id.status_sensitive_media_warning)
val sensitiveMediaShow = itemView.findViewById<View>(R.id.status_sensitive_media_button)
val mediaLabel = itemView.findViewById<TextView>(R.id.status_media_label)
if (mediaPreviewEnabled) {
if (statusDisplayOptions.mediaPreviewEnabled) {
// Hide the unused label.
mediaLabel.visibility = View.GONE
} else {
@ -86,13 +87,15 @@ class StatusViewHelper(private val itemView: View) {
}
val mediaPreviewUnloadedId = ThemeUtils.getDrawableId(context, R.attr.media_preview_unloaded_drawable, android.R.color.black)
val mediaPreviewUnloaded = ThemeUtils.getDrawable(context,
R.attr.media_preview_unloaded_drawable, android.R.color.black)
val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS)
for (i in 0 until n) {
val previewUrl = attachments[i].previewUrl
val description = attachments[i].description
val attachment = attachments[i]
val previewUrl = attachment.previewUrl
val description = attachment.description
if (TextUtils.isEmpty(description)) {
mediaPreviews[i].contentDescription = context.getString(R.string.action_view_media)
@ -104,35 +107,49 @@ class StatusViewHelper(private val itemView: View) {
if (TextUtils.isEmpty(previewUrl)) {
Glide.with(mediaPreviews[i])
.load(mediaPreviewUnloadedId)
.load(mediaPreviewUnloaded)
.centerInside()
.into(mediaPreviews[i])
} else {
val meta = attachments[i].meta
val placeholder = if (attachment.blurhash != null)
decodeBlurHash(context, attachment.blurhash)
else mediaPreviewUnloaded
val meta = attachment.meta
val focus = meta?.focus
if (showingContent) {
if (focus != null) { // If there is a focal point for this attachment:
mediaPreviews[i].setFocalPoint(focus)
if (focus != null) { // If there is a focal point for this attachment:
mediaPreviews[i].setFocalPoint(focus)
Glide.with(mediaPreviews[i])
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.addListener(mediaPreviews[i])
.into(mediaPreviews[i])
} else {
mediaPreviews[i].removeFocalPoint()
Glide.with(mediaPreviews[i])
.load(previewUrl)
.placeholder(mediaPreviewUnloadedId)
.centerInside()
.addListener(mediaPreviews[i])
.into(mediaPreviews[i])
Glide.with(mediaPreviews[i])
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.into(mediaPreviews[i])
}
} else {
mediaPreviews[i].removeFocalPoint()
Glide.with(mediaPreviews[i])
.load(previewUrl)
.placeholder(mediaPreviewUnloadedId)
.centerInside()
.into(mediaPreviews[i])
if (statusDisplayOptions.useBlurhash && attachment.blurhash != null) {
val blurhashBitmap = decodeBlurHash(context, attachment.blurhash)
mediaPreviews[i].setImageDrawable(blurhashBitmap)
} else {
mediaPreviews[i].setImageDrawable(ColorDrawable(ThemeUtils.getColor(
context, R.attr.sensitive_media_warning_background_color)))
}
}
}
val type = attachments[i].type
if ((type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) {
val type = attachment.type
if (showingContent
&& (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV)) {
mediaOverlays[i].visibility = View.VISIBLE
} else {
mediaOverlays[i].visibility = View.GONE
@ -158,13 +175,9 @@ class StatusViewHelper(private val itemView: View) {
} else {
val hiddenContentText: String = if (sensitive) {
context.getString(R.string.status_sensitive_media_template,
context.getString(R.string.status_sensitive_media_title),
context.getString(R.string.status_sensitive_media_directions))
context.getString(R.string.status_sensitive_media_title)
} else {
context.getString(R.string.status_sensitive_media_template,
context.getString(R.string.status_media_hidden_title),
context.getString(R.string.status_sensitive_media_directions))
context.getString(R.string.status_media_hidden_title)
}
sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
@ -175,11 +188,15 @@ class StatusViewHelper(private val itemView: View) {
previewListener.onContentHiddenChange(false)
v.visibility = View.GONE
sensitiveMediaWarning.visibility = View.VISIBLE
setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener,
false, mediaPreviewHeight)
}
sensitiveMediaWarning.setOnClickListener { v ->
previewListener.onContentHiddenChange(true)
v.visibility = View.GONE
sensitiveMediaShow.visibility = View.VISIBLE
setMediasPreview(statusDisplayOptions, attachments, sensitive, previewListener,
true, mediaPreviewHeight)
}
}

View File

@ -37,7 +37,7 @@ class BackgroundMessageView @JvmOverloads constructor(
* If [clickListener] is `null` then the button will be hidden.
*/
fun setup(@DrawableRes imageRes: Int, @StringRes messageRes: Int,
clickListener: ((v: View) -> Unit)?) {
clickListener: ((v: View) -> Unit)? = null) {
messageTextView.setText(messageRes)
messageTextView.setCompoundDrawablesWithIntrinsicBounds(0, imageRes, 0, 0)
button.setOnClickListener(clickListener)

View File

@ -50,7 +50,7 @@ defStyleAttr: Int = 0
/**
* Set the focal point for this view.
*/
fun setFocalPoint(focus: Attachment.Focus) {
fun setFocalPoint(focus: Attachment.Focus?) {
this.focus = focus
super.setScaleType(ScaleType.MATRIX)

View File

@ -17,26 +17,23 @@
package com.keylesspalace.tusky.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.Either.Left
import com.keylesspalace.tusky.util.Either.Right
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.withoutFirstWhich
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import io.reactivex.subjects.BehaviorSubject
import javax.inject.Inject
data class State(val accounts: Either<Throwable, List<Account>>, val searchResult: List<Account>?)
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() {
val state: Observable<State> get() = _state
private val _state = BehaviorSubject.createDefault(State(Right(listOf()), null))
private val disposable = CompositeDisposable()
fun load(listId: String) {
val state = _state.value!!
@ -45,7 +42,7 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi)
updateState { copy(accounts = Right(accounts)) }
}, { e ->
updateState { copy(accounts = Left(e)) }
}).addTo(disposable)
}).autoDispose()
}
}
@ -59,7 +56,7 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi)
Log.i(javaClass.simpleName,
"Failed to add account to the list: ${account.username}")
})
.addTo(disposable)
.autoDispose()
}
fun deleteAccountFromList(listId: String, accountId: String) {
@ -73,7 +70,7 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi)
}, {
Log.i(javaClass.simpleName, "Failed to remove account from thelist: $accountId")
})
.addTo(disposable)
.autoDispose()
}
fun search(query: String) {
@ -85,7 +82,7 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi)
updateState { copy(searchResult = result) }
}, {
updateState { copy(searchResult = listOf()) }
}).addTo(disposable)
}).autoDispose()
}
}

View File

@ -16,14 +16,12 @@
package com.keylesspalace.tusky.viewmodel
import androidx.lifecycle.ViewModel
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.withoutFirstWhich
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.replacedFirstWhich
import com.keylesspalace.tusky.util.withoutFirstWhich
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject
import java.io.IOException
@ -31,7 +29,7 @@ import java.net.ConnectException
import javax.inject.Inject
internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() {
internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() {
enum class LoadingState {
INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER
}
@ -46,7 +44,6 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi)
val events: Observable<Event> get() = _events
private val _state = BehaviorSubject.createDefault(State(listOf(), LoadingState.INITIAL))
private val _events = PublishSubject.create<Event>()
private val disposable = CompositeDisposable()
fun retryLoading() {
loadIfNeeded()
@ -71,7 +68,7 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi)
copy(loadingState = if (err is IOException || err is ConnectException)
LoadingState.ERROR_NETWORK else LoadingState.ERROR_OTHER)
}
}).addTo(disposable)
}).autoDispose()
}
fun createNewList(listName: String) {
@ -81,7 +78,7 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi)
}
}, {
sendEvent(Event.CREATE_ERROR)
}).addTo(disposable)
}).autoDispose()
}
fun renameList(listId: String, listName: String) {
@ -91,7 +88,7 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi)
}
}, {
sendEvent(Event.RENAME_ERROR)
}).addTo(disposable)
}).autoDispose()
}
fun deleteList(listId: String) {
@ -101,7 +98,7 @@ internal class ListsViewModel @Inject constructor(private val api: MastodonApi)
}
}, {
sendEvent(Event.DELETE_ERROR)
}).addTo(disposable)
}).autoDispose()
}
private inline fun updateState(crossinline fn: State.() -> State) {

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp" />
<solid android:color="?attr/sensitive_media_warning_background_color" />
</shape>

View File

@ -2,7 +2,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:id="@+id/activity_view_thread"
android:id="@+id/activityScheduledToot"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.AccountListActivity">
@ -15,7 +15,7 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ProgressBar
android:id="@+id/progress_bar"
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
@ -23,6 +23,18 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/scheduledTootList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/errorMessageView"
android:layout_width="wrap_content"
@ -36,18 +48,6 @@
tools:src="@drawable/elephant_error"
tools:visibility="visible" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/scheduled_toot_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -243,6 +243,7 @@
android:layout_height="@dimen/status_media_preview_height"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:background="@drawable/media_preview_outline"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/status_media_preview_2"
@ -314,11 +315,14 @@
android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/sensitive_media_warning_background_color"
android:background="@drawable/media_warning_bg"
android:gravity="center"
android:lineSpacingMultiplier="1.2"
android:orientation="vertical"
android:padding="8dp"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"

View File

@ -90,10 +90,11 @@
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/status_media_preview_margin_top"
android:layout_marginEnd="8dp"
android:background="@drawable/media_preview_outline"
app:layout_constraintEnd_toStartOf="@id/barrierEnd"
app:layout_constraintStart_toStartOf="@id/guideBegin"
app:layout_constraintTop_toBottomOf="@id/buttonToggleContent"
tools:visibility="gone">
tools:visibility="visible">
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_0"
@ -199,17 +200,21 @@
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
app:srcCompat="@drawable/ic_eye_24dp" />
app:srcCompat="@drawable/ic_eye_24dp"
tools:visibility="visible" />
<TextView
android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/sensitive_media_warning_background_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/media_warning_bg"
android:gravity="center"
android:lineSpacingMultiplier="1.2"
android:orientation="vertical"
android:padding="8dp"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"

View File

@ -197,11 +197,12 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/status_media_preview_margin_top"
android:background="@drawable/media_preview_outline"
android:importantForAccessibility="noHideDescendants"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@id/status_quote_inline_container"
tools:visibility="gone">
tools:visibility="visible">
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_0"
@ -311,14 +312,17 @@
<TextView
android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/sensitive_media_warning_background_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/media_warning_bg"
android:gravity="center"
android:importantForAccessibility="no"
android:lineSpacingMultiplier="1.2"
android:orientation="vertical"
android:padding="8dp"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"
@ -326,7 +330,9 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/status_sensitive_media_title"
tools:visibility="visible" />
<TextView
android:id="@+id/status_media_label_0"

View File

@ -215,6 +215,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="4dp"
android:background="@drawable/media_preview_outline"
android:importantForAccessibility="noHideDescendants"
app:layout_constraintTop_toBottomOf="@id/card_view">
@ -325,13 +326,16 @@
<TextView
android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/sensitive_media_warning_background_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/media_warning_bg"
android:gravity="center"
android:lineSpacingMultiplier="1.2"
android:orientation="vertical"
android:padding="8dp"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textColor="@android:color/white"
android:textSize="?attr/status_text_medium"
@ -339,7 +343,9 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/status_sensitive_media_title"
tools:visibility="visible" />
<TextView
android:id="@+id/status_media_label_0"

View File

@ -496,4 +496,6 @@
<string name="description_status_bookmarked">أضيف إلى الفواصل المرجعية</string>
<string name="select_list_title">اختر قائمة</string>
<string name="list">القائمة</string>
<string name="gradient_for_media">اظهر ألوانا متدرّجة للوسائط المخفية</string>
</resources>

View File

@ -5,16 +5,16 @@
<string name="error_empty">Això no pot estar buit.</string>
<string name="error_invalid_domain">El domini introduït no és vàlid</string>
<string name="error_failed_app_registration">L\'autenticació en aquesta instància ha fallat.</string>
<string name="error_no_web_browser_found">No s\'ha trobat cap navegador web per usar.</string>
<string name="error_no_web_browser_found">No s\'ha trobat cap navegador web per a usar.</string>
<string name="error_authorization_unknown">S\'ha produït un error d\'autorització no identificat.</string>
<string name="error_authorization_denied">L\'autorització s\'ha denegat.</string>
<string name="error_authorization_denied">S\'ha denegat l\'autorització.</string>
<string name="error_retrieving_oauth_token">L\'obtenció del testimoni d\'inici de sessió ha fallat.</string>
<string name="error_compose_character_limit">L\'estat és massa llarg!</string>
<string name="error_image_upload_size">El fitxer ha de ser inferior a 8MB.</string>
<string name="error_media_upload_type">Aquest tipus de fitxer no es pot pujar.</string>
<string name="error_media_upload_opening">Aquest tipus de fitxer no es pot obrir.</string>
<string name="error_media_upload_permission">Cal permís d\'accés al emmagatzematge.</string>
<string name="error_media_download_permission">Cal pemís d\'escriptura en el mitjà.</string>
<string name="error_media_download_permission">Cal permís d\'escriptura en el mitjà.</string>
<string name="error_media_upload_image_or_video">No es poden adjuntar imatges i vídeos en el mateix estat.</string>
<string name="error_media_upload_sending">La pujada ha fallat.</string>
@ -34,7 +34,7 @@
<string name="title_saved_toot">Esborranys</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s ha retootejat</string>
<string name="status_boosted_format">%s ha impulsat</string>
<string name="status_sensitive_media_title">Contingut sensible</string>
<string name="status_sensitive_media_directions">Fes clic per a visualitzar-lo</string>
<string name="status_content_warning_show_more">Mostra\'n més</string>
@ -42,7 +42,7 @@
<string name="footer_empty">No hi ha res aquí. Llisca avall per a actualitzar!</string>
<string name="notification_reblog_format">%s ha retootejat el teu toot</string>
<string name="notification_reblog_format">%s ha impulsat el teu toot</string>
<string name="notification_favourite_format">%s ha marcat com a preferit el teu toot</string>
<string name="notification_follow_format">%s et segueix</string>
@ -50,12 +50,12 @@
<string name="report_comment_hint">Cap comentari addicional?</string>
<string name="action_reply">Respon</string>
<string name="action_reblog">Retooteja</string>
<string name="action_reblog">Impulsa</string>
<string name="action_favourite">Preferit</string>
<string name="action_more">Més</string>
<string name="action_compose">Redacta</string>
<string name="action_login">Inicia sessió amb Mastodon</string>
<string name="action_logout">Tanca sessió</string>
<string name="action_logout">Tanca la sessió</string>
<string name="action_follow">Segueix</string>
<string name="action_unfollow">Deixa de seguir</string>
<string name="action_block">Bloca</string>
@ -107,7 +107,7 @@
<string name="hint_note">Biografia</string>
<string name="hint_search">Cerca…</string>
<string name="search_no_results">No hi ha cap resulat</string>
<string name="search_no_results">No hi ha cap resultat</string>
<string name="label_avatar">Avatar</string>
<string name="label_header">Capçalera</string>
@ -226,60 +226,60 @@
<string name="poll_vote">Vota</string>
<string name="error_sender_account_gone">Error enviant el toot.</string>
<string name="error_sender_account_gone">S\'ha produït un error en enviar el toot.</string>
<string name="title_tab_preferences">Pestanyes</string>
<string name="title_licenses">Llicències</string>
<string name="status_content_show_more">Ampliar</string>
<string name="status_content_show_more">Amplia</string>
<string name="action_quick_reply">Resposta ràpida</string>
<string name="action_unfavourite">Elimineu els preferits</string>
<string name="action_view_account_preferences">Preferències del compte</string>
<string name="action_edit_own_profile">Edita</string>
<string name="title_direct_messages">Missatges directes</string>
<string name="message_empty">No hi ha res aquí.</string>
<string name="action_unreblog">Elimineu els preferits</string>
<string name="error_network">S\'ha produït un error de conexió! Si us plau, comprova la teva conexió!</string>
<string name="error_video_upload_size">Els fitxers han de tenir menys de 40 MB.</string>
<string name="status_media_hidden_title">Multimèdia amagada</string>
<string name="status_content_show_less">Amagar</string>
<string name="action_unreblog">Elimina l\'impuls</string>
<string name="error_network">S\'ha produït un error de connexió! Comprova la connexió!</string>
<string name="error_video_upload_size">Els fitxers de vídeo han de tenir menys de 40 MB.</string>
<string name="status_media_hidden_title">Contingut multimèdia amagat</string>
<string name="status_content_show_less">Amaga</string>
<string name="action_logout_confirm">Estas segur de tancar la sessió de %1$s\?</string>
<string name="action_hide_reblogs">Amaga els retoots</string>
<string name="action_show_reblogs">Mostra els retootejats</string>
<string name="action_logout_confirm">Estàs segur de tancar la sessió de %1$s\?</string>
<string name="action_hide_reblogs">Amaga els impulsos</string>
<string name="action_show_reblogs">Mostra els impulsos</string>
<string name="action_delete_and_redraft">Elimina i reecririu</string>
<string name="action_open_drawer">Obre el menú</string>
<string name="action_toggle_visibility">Visibilitat del toot</string>
<string name="action_content_warning">Contingut sensible</string>
<string name="action_add_tab">Afegir una pestanya</string>
<string name="action_links">Enllaç</string>
<string name="action_add_tab">Afegeix una pestanya</string>
<string name="action_links">Enllaços</string>
<string name="action_mentions">Mencions</string>
<string name="action_hashtags">Hashtags</string>
<string name="action_open_faved_by">Mostra els favorits</string>
<string name="action_hashtags">Etiquetes</string>
<string name="action_open_faved_by">Mostra els preferits</string>
<string name="title_hashtags_dialog">Hashtags</string>
<string name="title_hashtags_dialog">Etiquetes</string>
<string name="title_mentions_dialog">Mencions</string>
<string name="title_links_dialog">Enllaços</string>
<string name="action_share_as">Compartir com </string>
<string name="download_media">Descargar el multimedia</string>
<string name="send_media_to">Compartir la imatge a …</string>
<string name="action_share_as">Comparteix com a</string>
<string name="download_media">Baixa el fitxer</string>
<string name="send_media_to">Comparteix la imatge a …</string>
<string name="status_sent">Enviat!</string>
<string name="state_follow_requested">Petició de seguiment enviada</string>
<string name="title_statuses_with_replies">Amb resposta</string>
<string name="action_emoji_keyboard">Teclat d\'emojis</string>
<string name="action_open_media_n">Obrir el media #%d</string>
<string name="action_open_media_n">Obre el fitxer #%d</string>
<string name="action_open_as">Obrir com %s</string>
<string name="downloading_media">Descarregant media</string>
<string name="action_open_as">Obre com a %s</string>
<string name="downloading_media">S\'està baixant el fitxer</string>
<string name="status_sent_long">Resposta enviada correctament.</string>
<string name="label_quick_reply">Resposta </string>
<string name="dialog_message_cancel_follow_request">Revocar la petició de seguiment\?</string>
<string name="label_quick_reply">Resposta…</string>
<string name="dialog_message_cancel_follow_request">Vols revocar la petició de seguiment\?</string>
<string name="dialog_delete_toot_warning">Vols eliminar aquest toot\?</string>
<string name="dialog_redraft_toot_warning">Esborrar i reescriure aquest toot\?</string>
<string name="dialog_redraft_toot_warning">Vols esborrar i reescriure aquest toot\?</string>
<string name="pref_title_notification_filter_poll">Finalització de les enquetes</string>
<string name="pref_title_app_theme">Tema</string>
@ -477,7 +477,7 @@
<string name="title_domain_mutes">Dominis ocults</string>
<string name="action_view_domain_mutes">Dominis ocults</string>
<string name="action_mute_domain">Silenciar %s</string>
<string name="action_mute_domain">Silencia %s</string>
<string name="confirmation_domain_unmuted">%s visible</string>
<string name="mute_domain_warning_dialog_ok">Amagar el domini sencer</string>
@ -521,15 +521,19 @@
<string name="title_bookmarks">Preferits</string>
<string name="title_scheduled_toot">Toots programats</string>
<string name="action_bookmark">Preferit</string>
<string name="action_edit">Editar</string>
<string name="action_edit">Edita</string>
<string name="action_view_bookmarks">Preferits</string>
<string name="action_access_scheduled_toot">Toots programats</string>
<string name="action_schedule_toot">Programar el toot</string>
<string name="action_reset_schedule">Reiniciar</string>
<string name="action_schedule_toot">Programa el toot</string>
<string name="action_reset_schedule">Reinicia</string>
<string name="about_powered_by_tusky">Desenvolupat per Tusky</string>
<string name="description_status_bookmarked">Afegit a les adreces d\'interès</string>
<string name="select_list_title">Seleccionar la llista</string>
<string name="list">Llista</string>
<string name="post_lookup_error_format">S\'ha produït un error en cercar la publicació %s</string>
<string name="gradient_for_media">Mostra degradats de colors per a contingut multimèdia ocult</string>
<string name="no_scheduled_status">No tens cap estat planificat.</string>
</resources>

View File

@ -458,7 +458,7 @@
<string name="title_bookmarks">Legosignoj</string>
<string name="title_scheduled_toot">Planitaj mesaĝoj</string>
<string name="action_bookmark">Aldoni al legosignoj</string>
<string name="action_bookmark">Aldoni al la legosignoj</string>
<string name="action_edit">Redakti</string>
<string name="action_view_bookmarks">Legosignoj</string>
<string name="action_access_scheduled_toot">Planitaj mesaĝoj</string>

View File

@ -39,7 +39,7 @@
<item name="status_quote_drawable">@drawable/ic_quote_24dp</item>
<item name="status_quote_disabled_drawable">@drawable/ic_quote_disabled_24dp</item>
<item name="content_warning_button">@drawable/toggle_small</item>
<item name="sensitive_media_warning_background_color">@color/color_background_dark</item>
<item name="sensitive_media_warning_background_color">#80000000</item>
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_dark</item>
<item name="android:listDivider">@drawable/status_divider_dark</item>
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_dark</item>

View File

@ -516,4 +516,8 @@
<string name="description_status_bookmarked">Bokmerke lagt til</string>
<string name="select_list_title">Velg liste</string>
<string name="list">Liste</string>
</resources>
<string name="gradient_for_media">Vis fargegradienter for skjult media</string>
<string name="no_scheduled_status">Du har ingen planlagte statuser.</string>
</resources>

View File

@ -470,4 +470,11 @@
<string name="action_reset_schedule">Återställ</string>
<string name="post_lookup_error_format">Fel vid uppslagning av status %s</string>
</resources>
<string name="title_bookmarks">Bokmärken</string>
<string name="action_bookmark">Bokmärk</string>
<string name="action_view_bookmarks">Bokmärken</string>
<string name="about_powered_by_tusky">Drivs av Tusky</string>
<string name="description_status_bookmarked">Bokmärkt</string>
<string name="select_list_title">Välj lista</string>
<string name="list">Lista</string>
</resources>

View File

@ -249,6 +249,7 @@
<string name="pref_title_language">Language</string>
<string name="pref_title_bot_overlay">Show indicator for bots</string>
<string name="pref_title_animate_gif_avatars">Animate GIF avatars</string>
<string name="gradient_for_media">Show colorful gradients for hidden media</string>
<string name="pref_title_status_filter">Timeline filtering</string>
<string name="pref_title_status_tabs">Tabs</string>
@ -573,4 +574,6 @@
<string name="edit_poll">Edit</string>
<string name="post_lookup_error_format">Error looking up post %s</string>
<string name="no_scheduled_status">You don\'t have any scheduled statuses.</string>
</resources>

View File

@ -91,9 +91,7 @@
<item name="status_quote_drawable">@drawable/ic_quote_24dp</item>
<item name="status_quote_disabled_drawable">@drawable/ic_quote_disabled_24dp</item>
<item name="content_warning_button">@drawable/toggle_small_light</item>
<item name="sensitive_media_warning_background_color">
@color/sensitive_media_warning_background_light
</item>
<item name="sensitive_media_warning_background_color">#80B0B0B0</item>
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_light</item>
<item name="android:listDivider">@drawable/status_divider_light</item>
<item name="conversation_thread_line_drawable">@drawable/conversation_thread_line_light
@ -192,7 +190,7 @@
<item name="recents_background_color">@color/toolbar_background_black</item>
<item name="window_background">@color/window_background_black</item>
<item name="sensitive_media_warning_background_color">@color/color_background_black</item>
<item name="sensitive_media_warning_background_color">#80000000</item>
<item name="account_header_background_color">@color/color_background_black</item>
<item name="report_status_background_color">@color/color_background_black</item>

View File

@ -60,6 +60,12 @@
android:title="@string/pref_title_animate_gif_avatars"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="useBlurhash"
android:title="@string/gradient_for_media"
app:singleLineTitle="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="showNotificationsFilter"