add option to always expand content warnings (#1422)
This commit is contained in:
parent
588775ff9b
commit
8834c22120
13 changed files with 742 additions and 210 deletions
|
@ -68,7 +68,7 @@ public class TuskyApplication extends Application implements HasAndroidInjector
|
|||
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
|
||||
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
|
||||
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
|
||||
AppDatabase.MIGRATION_16_17)
|
||||
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18)
|
||||
.build();
|
||||
accountManager = new AccountManager(appDatabase);
|
||||
serviceLocator = new ServiceLocator() {
|
||||
|
|
|
@ -1,199 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* 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 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 android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.SearchResults;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.ViewDataUtils;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class SearchResultsAdapter extends RecyclerView.Adapter {
|
||||
private static final int VIEW_TYPE_ACCOUNT = 0;
|
||||
private static final int VIEW_TYPE_STATUS = 1;
|
||||
private static final int VIEW_TYPE_HASHTAG = 2;
|
||||
|
||||
private List<Account> accountList;
|
||||
private List<Status> statusList;
|
||||
private List<StatusViewData.Concrete> concreteStatusList;
|
||||
private List<String> hashtagList;
|
||||
|
||||
private boolean mediaPreviewsEnabled;
|
||||
private boolean alwaysShowSensitiveMedia;
|
||||
private boolean useAbsoluteTime;
|
||||
private boolean showBotOverlay;
|
||||
private boolean animateAvatar;
|
||||
|
||||
private LinkListener linkListener;
|
||||
private StatusActionListener statusListener;
|
||||
|
||||
public SearchResultsAdapter(LinkListener linkListener,
|
||||
StatusActionListener statusListener,
|
||||
boolean mediaPreviewsEnabled,
|
||||
boolean alwaysShowSensitiveMedia,
|
||||
boolean useAbsoluteTime,
|
||||
boolean showBotOverlay,
|
||||
boolean animateAvatar) {
|
||||
|
||||
this.accountList = Collections.emptyList();
|
||||
this.statusList = Collections.emptyList();
|
||||
this.concreteStatusList = new ArrayList<>();
|
||||
this.hashtagList = Collections.emptyList();
|
||||
|
||||
this.mediaPreviewsEnabled = mediaPreviewsEnabled;
|
||||
this.alwaysShowSensitiveMedia = alwaysShowSensitiveMedia;
|
||||
this.useAbsoluteTime = useAbsoluteTime;
|
||||
this.showBotOverlay = showBotOverlay;
|
||||
this.animateAvatar = animateAvatar;
|
||||
|
||||
this.linkListener = linkListener;
|
||||
this.statusListener = statusListener;
|
||||
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_ACCOUNT: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_account, parent, false);
|
||||
return new AccountViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_HASHTAG: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_hashtag, parent, false);
|
||||
return new HashtagViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_STATUS: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status, parent, false);
|
||||
return new StatusViewHolder(view, useAbsoluteTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position >= accountList.size()) {
|
||||
if(position >= accountList.size() + concreteStatusList.size()) {
|
||||
HashtagViewHolder holder = (HashtagViewHolder) viewHolder;
|
||||
int index = position - accountList.size() - concreteStatusList.size();
|
||||
holder.setup(hashtagList.get(index), linkListener);
|
||||
} else {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
int index = position - accountList.size();
|
||||
holder.setupWithStatus(concreteStatusList.get(index), statusListener,
|
||||
mediaPreviewsEnabled, showBotOverlay, animateAvatar);
|
||||
}
|
||||
} else {
|
||||
AccountViewHolder holder = (AccountViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupLinkListener(linkListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return accountList.size() + hashtagList.size() + concreteStatusList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position >= accountList.size()) {
|
||||
if(position >= accountList.size() + concreteStatusList.size()) {
|
||||
return VIEW_TYPE_HASHTAG;
|
||||
} else {
|
||||
return VIEW_TYPE_STATUS;
|
||||
}
|
||||
} else {
|
||||
return VIEW_TYPE_ACCOUNT;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable public Status getStatusAtPosition(int position) {
|
||||
return statusList.get(position - accountList.size());
|
||||
}
|
||||
|
||||
@Nullable public StatusViewData.Concrete getConcreteStatusAtPosition(int position) {
|
||||
return concreteStatusList.get(position - accountList.size());
|
||||
}
|
||||
|
||||
public void updateStatusAtPosition(StatusViewData.Concrete status, int position, boolean doNotify) {
|
||||
concreteStatusList.set(position - accountList.size(), status);
|
||||
if(doNotify) {
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeStatusAtPosition(int position) {
|
||||
concreteStatusList.remove(position - accountList.size());
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
public void updateSearchResults(SearchResults results) {
|
||||
if (results != null) {
|
||||
accountList = results.getAccounts();
|
||||
statusList = results.getStatuses();
|
||||
concreteStatusList.clear();
|
||||
for(Status status: results.getStatuses()) {
|
||||
concreteStatusList.add(ViewDataUtils.statusToViewData(
|
||||
status,
|
||||
alwaysShowSensitiveMedia
|
||||
));
|
||||
}
|
||||
hashtagList = results.getHashtags();
|
||||
|
||||
} else {
|
||||
accountList = Collections.emptyList();
|
||||
statusList = Collections.emptyList();
|
||||
concreteStatusList.clear();
|
||||
hashtagList = Collections.emptyList();
|
||||
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static class HashtagViewHolder extends RecyclerView.ViewHolder {
|
||||
private TextView hashtag;
|
||||
|
||||
HashtagViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
hashtag = itemView.findViewById(R.id.hashtag);
|
||||
}
|
||||
|
||||
void setup(final String tag, final LinkListener listener) {
|
||||
hashtag.setText(String.format("#%s", tag));
|
||||
hashtag.setOnClickListener(v -> listener.onViewTag(tag));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,7 +45,9 @@ class SearchViewModel @Inject constructor(
|
|||
private val statusesRepository = SearchRepository<Pair<Status, StatusViewData.Concrete>>(mastodonApi)
|
||||
private val accountsRepository = SearchRepository<Account>(mastodonApi)
|
||||
private val hashtagsRepository = SearchRepository<HashTag>(mastodonApi)
|
||||
var alwaysShowSensitiveMedia: Boolean = activeAccount?.alwaysShowSensitiveMedia
|
||||
val alwaysShowSensitiveMedia: Boolean = activeAccount?.alwaysShowSensitiveMedia
|
||||
?: false
|
||||
val alwaysOpenSpoiler: Boolean = activeAccount?.alwaysOpenSpoiler
|
||||
?: false
|
||||
|
||||
private val repoResultStatus = MutableLiveData<Listing<Pair<Status, StatusViewData.Concrete>>>()
|
||||
|
@ -67,7 +69,7 @@ class SearchViewModel @Inject constructor(
|
|||
fun search(query: String?) {
|
||||
loadedStatuses.clear()
|
||||
repoResultStatus.value = statusesRepository.getSearchData(SearchType.Status, query, disposables, initialItems = loadedStatuses) {
|
||||
(it?.statuses?.map { status -> Pair(status, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia)!!) }
|
||||
(it?.statuses?.map { status -> Pair(status, ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia, alwaysOpenSpoiler)!!) }
|
||||
?: emptyList())
|
||||
.apply {
|
||||
loadedStatuses.addAll(this)
|
||||
|
|
|
@ -48,6 +48,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
|
|||
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
|
||||
var defaultMediaSensitivity: Boolean = false,
|
||||
var alwaysShowSensitiveMedia: Boolean = false,
|
||||
var alwaysOpenSpoiler: Boolean = false,
|
||||
var mediaPreviewEnabled: Boolean = true,
|
||||
var lastNotificationId: String = "0",
|
||||
var activeNotifications: String = "[]",
|
||||
|
|
|
@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 17)
|
||||
}, version = 18)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract TootDao tootDao();
|
||||
|
@ -293,4 +293,11 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_17_18 = new Migration(17, 18) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `alwaysOpenSpoiler` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -162,6 +162,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
private boolean bottomLoading;
|
||||
private String bottomId;
|
||||
private boolean alwaysShowSensitiveMedia;
|
||||
private boolean alwaysOpenSpoiler;
|
||||
private boolean showNotificationsFilter;
|
||||
|
||||
// Each element is either a Notification for loading data or a Placeholder
|
||||
|
@ -173,7 +174,8 @@ public class NotificationsFragment extends SFragment implements
|
|||
Notification notification = input.asRight();
|
||||
return ViewDataUtils.notificationToViewData(
|
||||
notification,
|
||||
alwaysShowSensitiveMedia
|
||||
alwaysShowSensitiveMedia,
|
||||
alwaysOpenSpoiler
|
||||
);
|
||||
} else {
|
||||
return new NotificationViewData.Placeholder(input.asLeft().id, false);
|
||||
|
@ -236,6 +238,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
|
||||
dataSource, this, this);
|
||||
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
|
||||
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
|
||||
boolean mediaPreviewEnabled = accountManager.getActiveAccount().getMediaPreviewEnabled();
|
||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
||||
|
|
|
@ -166,6 +166,7 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
private boolean didLoadEverythingBottom;
|
||||
private boolean alwaysShowSensitiveMedia;
|
||||
private boolean alwaysOpenSpoiler;
|
||||
private boolean initialUpdateFailed = false;
|
||||
|
||||
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
|
||||
|
@ -176,7 +177,8 @@ public class TimelineFragment extends SFragment implements
|
|||
if (status != null) {
|
||||
return ViewDataUtils.statusToViewData(
|
||||
status,
|
||||
alwaysShowSensitiveMedia
|
||||
alwaysShowSensitiveMedia,
|
||||
alwaysOpenSpoiler
|
||||
);
|
||||
} else {
|
||||
Placeholder placeholder = input.asLeft();
|
||||
|
@ -340,6 +342,7 @@ 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);
|
||||
|
|
|
@ -92,6 +92,7 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
private ThreadAdapter adapter;
|
||||
private String thisThreadsStatusId;
|
||||
private boolean alwaysShowSensitiveMedia;
|
||||
private boolean alwaysOpenSpoiler;
|
||||
|
||||
private int statusIndex = 0;
|
||||
|
||||
|
@ -101,7 +102,8 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
public StatusViewData.Concrete apply(Status input) {
|
||||
return ViewDataUtils.statusToViewData(
|
||||
input,
|
||||
alwaysShowSensitiveMedia
|
||||
alwaysShowSensitiveMedia,
|
||||
alwaysOpenSpoiler
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -149,6 +151,7 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
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);
|
||||
|
|
|
@ -65,6 +65,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
private lateinit var defaultPostPrivacyPreference: ListPreference
|
||||
private lateinit var defaultMediaSensitivityPreference: SwitchPreferenceCompat
|
||||
private lateinit var alwaysShowSensitiveMediaPreference: SwitchPreferenceCompat
|
||||
private lateinit var alwaysOpenSpoilerPreference: SwitchPreferenceCompat
|
||||
private lateinit var mediaPreviewEnabledPreference: SwitchPreferenceCompat
|
||||
private lateinit var homeFiltersPreference: Preference
|
||||
private lateinit var notificationFiltersPreference: Preference
|
||||
|
@ -85,6 +86,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
defaultMediaSensitivityPreference = requirePreference("defaultMediaSensitivity") as SwitchPreferenceCompat
|
||||
mediaPreviewEnabledPreference = requirePreference("mediaPreviewEnabled") as SwitchPreferenceCompat
|
||||
alwaysShowSensitiveMediaPreference = requirePreference("alwaysShowSensitiveMedia") as SwitchPreferenceCompat
|
||||
alwaysOpenSpoilerPreference = requirePreference("alwaysOpenSpoiler") as SwitchPreferenceCompat
|
||||
homeFiltersPreference = requirePreference("homeFilters")
|
||||
notificationFiltersPreference = requirePreference("notificationFilters")
|
||||
publicFiltersPreference = requirePreference("publicFilters")
|
||||
|
@ -109,6 +111,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
defaultMediaSensitivityPreference.onPreferenceChangeListener = this
|
||||
mediaPreviewEnabledPreference.onPreferenceChangeListener = this
|
||||
alwaysShowSensitiveMediaPreference.onPreferenceChangeListener = this
|
||||
alwaysOpenSpoilerPreference.onPreferenceChangeListener = this
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -124,6 +127,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
|
||||
mediaPreviewEnabledPreference.isChecked = it.mediaPreviewEnabled
|
||||
alwaysShowSensitiveMediaPreference.isChecked = it.alwaysShowSensitiveMedia
|
||||
alwaysOpenSpoilerPreference.isChecked = it.alwaysOpenSpoiler
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -150,6 +154,12 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
accountManager.saveAccount(it)
|
||||
}
|
||||
}
|
||||
alwaysOpenSpoilerPreference -> {
|
||||
accountManager.activeAccount?.let {
|
||||
it.alwaysOpenSpoiler = newValue as Boolean
|
||||
accountManager.saveAccount(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eventHub.dispatch(PreferenceChangedEvent(preference.key))
|
||||
|
|
|
@ -29,7 +29,8 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
|
|||
public final class ViewDataUtils {
|
||||
@Nullable
|
||||
public static StatusViewData.Concrete statusToViewData(@Nullable Status status,
|
||||
boolean alwaysShowSensitiveMedia) {
|
||||
boolean alwaysShowSensitiveMedia,
|
||||
boolean alwaysOpenSpoiler) {
|
||||
if (status == null) return null;
|
||||
Status visibleStatus = status.getReblog() == null ? status : status.getReblog();
|
||||
return new StatusViewData.Builder().setId(status.getId())
|
||||
|
@ -42,7 +43,7 @@ public final class ViewDataUtils {
|
|||
.setInReplyToId(visibleStatus.getInReplyToId())
|
||||
.setFavourited(visibleStatus.getFavourited())
|
||||
.setReblogged(visibleStatus.getReblogged())
|
||||
.setIsExpanded(false)
|
||||
.setIsExpanded(alwaysOpenSpoiler)
|
||||
.setIsShowingSensitiveContent(false)
|
||||
.setMentions(visibleStatus.getMentions())
|
||||
.setNickname(visibleStatus.getAccount().getUsername())
|
||||
|
@ -67,14 +68,16 @@ public final class ViewDataUtils {
|
|||
}
|
||||
|
||||
public static NotificationViewData.Concrete notificationToViewData(Notification notification,
|
||||
boolean alwaysShowSensitiveData) {
|
||||
boolean alwaysShowSensitiveData,
|
||||
boolean alwaysOpenSpoiler) {
|
||||
return new NotificationViewData.Concrete(
|
||||
notification.getType(),
|
||||
notification.getId(),
|
||||
notification.getAccount(),
|
||||
statusToViewData(
|
||||
notification.getStatus(),
|
||||
alwaysShowSensitiveData
|
||||
alwaysShowSensitiveData,
|
||||
alwaysOpenSpoiler
|
||||
),
|
||||
false
|
||||
);
|
||||
|
|
|
@ -313,6 +313,7 @@
|
|||
|
||||
<string name="follows_you">Follows you</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">Always show sensitive content</string>
|
||||
<string name="pref_title_alway_open_spoiler">Always expand toots marked with content warnings</string>
|
||||
<string name="title_media">Media</string>
|
||||
<string name="replying_to">Replying to @%s</string>
|
||||
<string name="load_more_placeholder_text">load more</string>
|
||||
|
|
|
@ -50,6 +50,11 @@
|
|||
android:key="alwaysShowSensitiveMedia"
|
||||
android:title="@string/pref_title_alway_show_sensitive_media"
|
||||
app:singleLineTitle="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="alwaysOpenSpoiler"
|
||||
android:title="@string/pref_title_alway_open_spoiler"
|
||||
app:singleLineTitle="false" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/pref_title_timeline_filters">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue