Merge remote-tracking branch 'tuskyapp/develop'
This commit is contained in:
commit
9efc4a4bc9
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" -> {
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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> {
|
||||
|
|
|
@ -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>? {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue