add option to always expand content warnings (#1422)

This commit is contained in:
Konrad Pozniak 2019-07-28 19:59:52 +02:00 committed by GitHub
commit 8834c22120
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 742 additions and 210 deletions

View file

@ -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() {

View file

@ -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));
}
}
}

View file

@ -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)

View file

@ -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 = "[]",

View file

@ -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");
}
};
}

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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))

View file

@ -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
);

View file

@ -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>

View file

@ -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">