Compare commits
171 Commits
v1.1.5+for
...
v1.1.5+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
333c38c64d | ||
|
|
ca5827e3f8 | ||
|
|
4884667484 | ||
|
|
a2f687898c | ||
|
|
defd038064 | ||
|
|
f88b65f479 | ||
|
|
f65d56361f | ||
|
|
255155b55a | ||
|
|
ee2e39462a | ||
|
|
32b459ae77 | ||
|
|
c1b79da4a7 | ||
|
|
65dfd8667d | ||
|
|
12558c3c18 | ||
|
|
dae347a29f | ||
|
|
c51be5f199 | ||
|
|
fc1bd14f70 | ||
|
|
bd39ed3754 | ||
|
|
50029c7f73 | ||
|
|
e2c907eb10 | ||
|
|
937747e11b | ||
|
|
85b6bc79a3 | ||
|
|
ec9d41fbbd | ||
|
|
847d966daa | ||
|
|
618840c76a | ||
|
|
33d856562d | ||
|
|
9873e9ede5 | ||
|
|
63dad42bf3 | ||
|
|
dad58f8245 | ||
|
|
647a7d70cd | ||
|
|
f49c7dff00 | ||
|
|
72f638c96c | ||
|
|
35e0897869 | ||
|
|
e22cb07d63 | ||
|
|
c2df989217 | ||
|
|
31d0bfb434 | ||
|
|
14e639aa8a | ||
|
|
6c24e06157 | ||
|
|
53ce4276f6 | ||
|
|
423e919e16 | ||
|
|
c6cd424f30 | ||
|
|
e282d54f99 | ||
|
|
29ad08f2ea | ||
|
|
5c2f72a706 | ||
|
|
b153a64373 | ||
|
|
1124486f1f | ||
|
|
c757b1ffea | ||
|
|
932655eeb6 | ||
|
|
21d57b25c9 | ||
|
|
bed572f343 | ||
|
|
c7483a6b20 | ||
|
|
cdb1e26a4d | ||
|
|
ce1a450ccb | ||
|
|
dfc244ff41 | ||
|
|
9c3e2f5deb | ||
|
|
452b286352 | ||
|
|
6deca645de | ||
|
|
49fd1aba76 | ||
|
|
7bc951ba67 | ||
|
|
a70e73a8cb | ||
|
|
cf345356a5 | ||
|
|
3da3967afa | ||
|
|
a12f09a38a | ||
|
|
a7302cc3e1 | ||
|
|
9aed2a96dc | ||
|
|
dbe49134e1 | ||
|
|
089d176704 | ||
|
|
4e482ef6fa | ||
|
|
c64397a613 | ||
|
|
6c0d4778b7 | ||
|
|
b94c1f4a82 | ||
|
|
a29a072e53 | ||
|
|
4f435c6957 | ||
|
|
2a6115f6d9 | ||
|
|
4cbc1e3664 | ||
|
|
1053d2ac0c | ||
|
|
0123b17602 | ||
|
|
1dace6ead9 | ||
|
|
3287cf69c1 | ||
|
|
a2854524a9 | ||
|
|
71c06c0762 | ||
|
|
e30df6067d | ||
|
|
912a354b1c | ||
|
|
71f830ea82 | ||
|
|
bd109a9139 | ||
|
|
c82b4445ff | ||
|
|
9a7d149dae | ||
|
|
c66e576461 | ||
|
|
2a47d2fe77 | ||
|
|
331d490f4f | ||
|
|
8d722a2130 | ||
|
|
6863363452 | ||
|
|
10e66a58eb | ||
|
|
3a5c27eadc | ||
|
|
8c6bce4f73 | ||
|
|
17e1cd1fe9 | ||
|
|
d7aceffc8f | ||
|
|
bcb3e217cd | ||
|
|
229c19664c | ||
|
|
8bdbb2adef | ||
|
|
907c5a2ca1 | ||
|
|
2a2bfebf48 | ||
|
|
eba59549ec | ||
|
|
9478a71693 | ||
|
|
b01d7a417a | ||
|
|
80c77292ed | ||
|
|
488e6dda04 | ||
|
|
689f676668 | ||
|
|
a06db9a3ab | ||
|
|
a7283cbed8 | ||
|
|
bc70d5e212 | ||
|
|
441686740a | ||
|
|
ac0df083f2 | ||
|
|
e10762d5fa | ||
|
|
0ca4663c29 | ||
|
|
7efd9341b1 | ||
|
|
3d8693b2bd | ||
|
|
a3b5f3c926 | ||
|
|
f9f4a1d1ef | ||
|
|
dd536002d0 | ||
|
|
4f8e381c84 | ||
|
|
3b6b212c9e | ||
|
|
bf429ee263 | ||
|
|
e7b1301b71 | ||
|
|
6726e9523c | ||
|
|
3ffcc7cef2 | ||
|
|
e6232f6d3b | ||
|
|
5efc431192 | ||
|
|
c3b75782b1 | ||
|
|
ec6f3f0cc3 | ||
|
|
136c3cfb4a | ||
|
|
79d1dbd3b7 | ||
|
|
f2e1663c41 | ||
|
|
5c73f37599 | ||
|
|
7f239abf2f | ||
|
|
a07f7c232a | ||
|
|
4a60a5190f | ||
|
|
69986fd869 | ||
|
|
e2ca572d45 | ||
|
|
746e41fdbc | ||
|
|
091f1f1e8c | ||
|
|
844ec185a6 | ||
|
|
5622eaed83 | ||
|
|
dbf25da1db | ||
|
|
d35a416084 | ||
|
|
098acb85e4 | ||
|
|
9d67337913 | ||
|
|
914861775a | ||
|
|
c12a6eaee6 | ||
|
|
5de23581fe | ||
|
|
413141df1e | ||
|
|
5f48870a90 | ||
|
|
86e781cdea | ||
|
|
34ebd9219f | ||
|
|
41688c4670 | ||
|
|
0d30cd973e | ||
|
|
5ed80ca40a | ||
|
|
78958085c3 | ||
|
|
a1798b6666 | ||
|
|
baa7dd6302 | ||
|
|
ba93e5bac3 | ||
|
|
2358d3c602 | ||
|
|
cf61626901 | ||
|
|
349a1115a6 | ||
|
|
8fa4980ba5 | ||
|
|
099d0ccf94 | ||
|
|
35a1de7888 | ||
|
|
6fc850b5ba | ||
|
|
96f13defd4 | ||
|
|
36dd07aa38 | ||
|
|
6a831539ad | ||
|
|
c679f5529e |
@@ -152,6 +152,8 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* [Display server announcements](https://github.com/sk22/megalodon/commit/84179bc207d6b69cc2a770a3c28fa0a39b0b54e8)
|
||||
* [Create](https://github.com/sk22/megalodon/commit/294595513a45037359b31377aafc25ae5b58d8e7), [edit](https://github.com/sk22/megalodon/commit/d47797bf7ac8cff3f9ba1cfee219a1bb2af21da6) and [delete](https://github.com/sk22/megalodon/commit/54c29fd787fc2cd0dfd2787ad796b8190f795973) lists
|
||||
* [Soft-blocking (by blocking and immediately unblocking)](https://github.com/sk22/megalodon/commit/e75d350b7a2709259e9fc5138e0e1f361bdb0972)
|
||||
* [Pinnable custom timelines](https://github.com/sk22/megalodon/pull/338/commits)
|
||||
* Support for local-only posts
|
||||
|
||||
|
||||
### Behavior
|
||||
@@ -175,6 +177,8 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* [Preserve whitespaces in HTML](https://github.com/sk22/megalodon/commit/7d876bddc7a07d98f0fecbf62b13bdb9fcce3412)
|
||||
* [Long-click to copy links](https://github.com/sk22/megalodon/commit/b32e32274923a94742a9926ef38785f746d41405)
|
||||
* Improved filtering using Mastodon 4.0 API: [#202](https://github.com/sk22/megalodon/pull/202), [#212](https://github.com/sk22/megalodon/pull/212), [#255](https://github.com/sk22/megalodon/pull/255) by [@thiagojedi](https://github.com/thiagojedi)
|
||||
* [Support admin notifications](https://github.com/sk22/megalodon/commit/c12a6eaee6b609bc53eb0a45d9199f37d5241801) and [notifications for edited reblogged posts](https://github.com/sk22/megalodon/commit/900e8fb2e9353002c16d15e06b78d2731e121601)
|
||||
* [Android file opener added back in addition to image picker](https://github.com/sk22/megalodon/commit/3a6ace53d5ab01e28077c9c930cb6ed487b78031)
|
||||
|
||||
|
||||
### Visual
|
||||
@@ -190,6 +194,8 @@ There's also a handful of custom strings exclusive to this projects that would n
|
||||
* [Animations for interaction buttons](https://github.com/mastodon/mastodon-android/compare/master...sk22:megalodon:feature/animate-buttons)
|
||||
* [Dedicated icons for different notification types](https://github.com/sk22/megalodon/pull/178) by [@florian-obernberger](https://github.com/florian-obernberger)
|
||||
* Scale text according to system settings
|
||||
* Header in timeline for followed hashtags
|
||||
* [Indicator for missing alt texts](https://github.com/sk22/megalodon/commit/c0c276f03e793b78c478c17dfdef24a66ef7cedb)
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
@@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=false
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 70
|
||||
versionName "1.1.5+fork.70"
|
||||
versionCode 74
|
||||
versionName "1.1.5+fork.74"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
@@ -70,6 +70,7 @@ dependencies {
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
implementation 'de.psdev:async-otto:1.0.3'
|
||||
implementation 'org.parceler:parceler-api:1.1.12'
|
||||
implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0'
|
||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
|
||||
@@ -12,8 +12,10 @@ import org.joinmastodon.android.model.TimelineDefinition;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class GlobalUserPreferences{
|
||||
public static boolean playGifs;
|
||||
@@ -22,6 +24,7 @@ public class GlobalUserPreferences{
|
||||
public static boolean showReplies;
|
||||
public static boolean showBoosts;
|
||||
public static boolean loadNewPosts;
|
||||
public static boolean showNewPostsButton;
|
||||
public static boolean showInteractionCounts;
|
||||
public static boolean alwaysExpandContentWarnings;
|
||||
public static boolean disableMarquee;
|
||||
@@ -36,6 +39,10 @@ public class GlobalUserPreferences{
|
||||
public static boolean showAltIndicator;
|
||||
public static boolean showNoAltIndicator;
|
||||
public static boolean enablePreReleases;
|
||||
public static boolean prefixRepliesWithRe;
|
||||
public static boolean bottomEncoding;
|
||||
public static boolean collapseLongPosts;
|
||||
public static boolean spectatorMode;
|
||||
public static String publishButtonText;
|
||||
public static ThemePreference theme;
|
||||
public static ColorPreference color;
|
||||
@@ -44,6 +51,8 @@ public class GlobalUserPreferences{
|
||||
private final static Type pinnedTimelinesType = new TypeToken<Map<String, List<TimelineDefinition>>>() {}.getType();
|
||||
public static Map<String, List<String>> recentLanguages;
|
||||
public static Map<String, List<TimelineDefinition>> pinnedTimelines;
|
||||
public static Set<String> accountsWithLocalOnlySupport;
|
||||
public static Set<String> accountsInGlitchMode;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
@@ -63,6 +72,7 @@ public class GlobalUserPreferences{
|
||||
showReplies=prefs.getBoolean("showReplies", true);
|
||||
showBoosts=prefs.getBoolean("showBoosts", true);
|
||||
loadNewPosts=prefs.getBoolean("loadNewPosts", true);
|
||||
showNewPostsButton=prefs.getBoolean("showNewPostsButton", true);
|
||||
showInteractionCounts=prefs.getBoolean("showInteractionCounts", false);
|
||||
alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false);
|
||||
disableMarquee=prefs.getBoolean("disableMarquee", false);
|
||||
@@ -77,10 +87,16 @@ public class GlobalUserPreferences{
|
||||
showAltIndicator=prefs.getBoolean("showAltIndicator", true);
|
||||
showNoAltIndicator=prefs.getBoolean("showNoAltIndicator", true);
|
||||
enablePreReleases=prefs.getBoolean("enablePreReleases", false);
|
||||
prefixRepliesWithRe=prefs.getBoolean("prefixRepliesWithRe", false);
|
||||
bottomEncoding=prefs.getBoolean("bottomEncoding", false);
|
||||
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
|
||||
spectatorMode=prefs.getBoolean("spectatorMode", false);
|
||||
publishButtonText=prefs.getString("publishButtonText", "");
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
|
||||
pinnedTimelines=fromJson(prefs.getString("pinnedTimelines", null), pinnedTimelinesType, new HashMap<>());
|
||||
accountsWithLocalOnlySupport=prefs.getStringSet("accountsWithLocalOnlySupport", new HashSet<>());
|
||||
accountsInGlitchMode=prefs.getStringSet("accountsInGlitchMode", new HashSet<>());
|
||||
|
||||
try {
|
||||
color=ColorPreference.valueOf(prefs.getString("color", ColorPreference.PINK.name()));
|
||||
@@ -97,6 +113,7 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("showReplies", showReplies)
|
||||
.putBoolean("showBoosts", showBoosts)
|
||||
.putBoolean("loadNewPosts", loadNewPosts)
|
||||
.putBoolean("showNewPostsButton", showNewPostsButton)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putBoolean("showInteractionCounts", showInteractionCounts)
|
||||
.putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings)
|
||||
@@ -111,11 +128,17 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("showAltIndicator", showAltIndicator)
|
||||
.putBoolean("showNoAltIndicator", showNoAltIndicator)
|
||||
.putBoolean("enablePreReleases", enablePreReleases)
|
||||
.putBoolean("prefixRepliesWithRe", prefixRepliesWithRe)
|
||||
.putBoolean("collapseLongPosts", collapseLongPosts)
|
||||
.putBoolean("spectatorMode", spectatorMode)
|
||||
.putString("publishButtonText", publishButtonText)
|
||||
.putBoolean("bottomEncoding", bottomEncoding)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.putString("color", color.name())
|
||||
.putString("recentLanguages", gson.toJson(recentLanguages))
|
||||
.putString("pinnedTimelines", gson.toJson(pinnedTimelines))
|
||||
.putStringSet("accountsWithLocalOnlySupport", accountsWithLocalOnlySupport)
|
||||
.putStringSet("accountsInGlitchMode", accountsInGlitchMode)
|
||||
.apply();
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ public class MainActivity extends FragmentStackActivity{
|
||||
);
|
||||
Bundle currentArgs = currentFragment.getArguments();
|
||||
if (this.fragmentContainers.size() == 1
|
||||
&& currentArgs != null
|
||||
&& currentArgs.getBoolean("_can_go_back", false)
|
||||
&& currentArgs.containsKey("account")) {
|
||||
Bundle args = new Bundle();
|
||||
|
||||
@@ -153,6 +153,8 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
case POLL -> R.drawable.ic_fluent_poll_24_filled;
|
||||
case STATUS -> R.drawable.ic_fluent_chat_24_filled;
|
||||
case UPDATE -> R.drawable.ic_fluent_history_24_filled;
|
||||
case REPORT -> R.drawable.ic_fluent_warning_24_filled;
|
||||
case SIGN_UP -> R.drawable.ic_fluent_person_available_24_filled;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class SetAccountFollowed extends MastodonAPIRequest<Relationship>{
|
||||
public SetAccountFollowed(String id, boolean followed, boolean showReblogs){
|
||||
this(id, followed, showReblogs, false);
|
||||
}
|
||||
|
||||
public SetAccountFollowed(String id, boolean followed, boolean showReblogs, boolean notify){
|
||||
super(HttpMethod.POST, "/accounts/"+id+"/"+(followed ? "follow" : "unfollow"), Relationship.class);
|
||||
if(followed)
|
||||
|
||||
@@ -39,6 +39,7 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
public Poll poll;
|
||||
public String inReplyToId;
|
||||
public boolean sensitive;
|
||||
public boolean localOnly;
|
||||
public String spoilerText;
|
||||
public StatusPrivacy visibility;
|
||||
public Instant scheduledAt;
|
||||
|
||||
@@ -11,12 +11,15 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
@@ -56,8 +59,8 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(getActivity()==null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.ACCOUNT)).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
|
||||
@@ -92,6 +92,7 @@ public class AnnouncementsFragment extends BaseStatusListFragment<Announcement>
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Announcement> result){
|
||||
if (getActivity() == null) return;
|
||||
List<Announcement> unread = result.stream().filter(a -> !a.read).collect(toList());
|
||||
List<Announcement> read = result.stream().filter(a -> a.read).collect(toList());
|
||||
onDataLoaded(unread, true);
|
||||
|
||||
@@ -16,6 +16,7 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -40,6 +41,7 @@ import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -56,6 +58,8 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
@@ -74,9 +78,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
protected HashMap<String, Account> knownAccounts=new HashMap<>();
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected Rect tmpRect=new Rect();
|
||||
protected ImageButton fab;
|
||||
|
||||
public BaseStatusListFragment(){
|
||||
super(20);
|
||||
if (withComposeButton()) setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
protected boolean withComposeButton() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,6 +100,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return adapter=new DisplayItemsAdapter();
|
||||
@@ -313,6 +325,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
((UsableRecyclerView) list).setIncludeMarginsInItemHitbox(true);
|
||||
updateToolbar();
|
||||
|
||||
if (withComposeButton()) {
|
||||
fab = view.findViewById(R.id.fab);
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(this::onFabLongClick);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -487,6 +506,23 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
updateImagesSpoilerState(status, holder.getItemID());
|
||||
}
|
||||
|
||||
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
|
||||
if (holder.getItem().status.textExpandable != expandable && list != null) {
|
||||
holder.getItem().status.textExpandable = expandable;
|
||||
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
|
||||
if (header != null) header.rebind();
|
||||
holder.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
public void onToggleExpanded(Status status, String itemID) {
|
||||
status.textExpanded = !status.textExpanded;
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if (text != null) text.rebind();
|
||||
if (header != null) header.rebind();
|
||||
}
|
||||
|
||||
protected void updateImagesSpoilerState(Status status, String itemID){
|
||||
ArrayList<Integer> updatedPositions=new ArrayList<>();
|
||||
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){
|
||||
@@ -504,6 +540,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
|
||||
public void onGapClick(GapStatusDisplayItem.Holder item){}
|
||||
|
||||
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
|
||||
int startPos = warning.getAbsoluteAdapterPosition();
|
||||
displayItems.remove(startPos);
|
||||
displayItems.addAll(startPos, warning.filteredItems);
|
||||
adapter.notifyItemRangeInserted(startPos, warning.filteredItems.size() - 1);
|
||||
if (startPos == 0) scrollToTop();
|
||||
warning.getItem().status.filterRevealed = true;
|
||||
}
|
||||
|
||||
public String getAccountID(){
|
||||
return accountID;
|
||||
}
|
||||
@@ -619,6 +664,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
currentPhotoViewer.onPause();
|
||||
}
|
||||
|
||||
protected void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
protected boolean onFabLongClick(View v) {
|
||||
return UiUtils.pickAccountForCompose(getActivity(), accountID);
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public DisplayItemsAdapter(){
|
||||
|
||||
@@ -25,6 +25,7 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
|
||||
@@ -3,9 +3,9 @@ package org.joinmastodon.android.fragments;
|
||||
import static org.joinmastodon.android.GlobalUserPreferences.recentLanguages;
|
||||
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.DRAFTS_AFTER_INSTANT;
|
||||
import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDraftInstant;
|
||||
import static org.joinmastodon.android.ui.utils.UiUtils.isPhotoPickerAvailable;
|
||||
import static org.joinmastodon.android.utils.MastodonLanguage.allLanguages;
|
||||
import static org.joinmastodon.android.utils.MastodonLanguage.defaultRecentLanguages;
|
||||
import static android.os.ext.SdkExtensions.getExtensionVersion;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -42,6 +42,7 @@ import android.text.TextWatcher;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -66,6 +67,7 @@ import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.bottomSoftwareFoundation.bottom.Bottom;
|
||||
import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -115,6 +117,7 @@ import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
|
||||
import org.joinmastodon.android.utils.MastodonLanguage;
|
||||
import org.joinmastodon.android.utils.StatusTextEncoder;
|
||||
import org.parceler.Parcel;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -151,19 +154,21 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
private static final int SCHEDULED_STATUS_OPENED_RESULT=161;
|
||||
private static final int MAX_ATTACHMENTS=4;
|
||||
private static final String GLITCH_LOCAL_ONLY_SUFFIX = "👁";
|
||||
private static final Pattern GLITCH_LOCAL_ONLY_PATTERN = Pattern.compile("[\\s\\S]*" + GLITCH_LOCAL_ONLY_SUFFIX + "[\uFE00-\uFE0F]*");
|
||||
private static final String TAG="ComposeFragment";
|
||||
|
||||
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
public static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
// from https://github.com/mastodon/mastodon-ios/blob/main/Mastodon/Helper/MastodonRegex.swift
|
||||
private static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
|
||||
private static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
|
||||
public static final Pattern AUTO_COMPLETE_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+)|:([a-zA-Z0-9_]+))");
|
||||
public static final Pattern HIGHLIGHT_PATTERN=Pattern.compile("(?<!\\w)(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))");
|
||||
|
||||
@SuppressLint("NewApi") // this class actually exists on 6.0
|
||||
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
|
||||
|
||||
private SizeListenerLinearLayout contentView;
|
||||
private TextView selfName, selfUsername;
|
||||
private TextView selfName, selfUsername, selfExtraText, extraText;
|
||||
private ImageView selfAvatar;
|
||||
private Account self;
|
||||
private String instanceDomain;
|
||||
@@ -212,6 +217,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private View sendingOverlay;
|
||||
private WindowManager wm;
|
||||
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC;
|
||||
private boolean localOnly;
|
||||
private ComposeAutocompleteSpan currentAutocompleteSpan;
|
||||
private FrameLayout mainEditTextWrap;
|
||||
private ComposeAutocompleteViewController autocompleteViewController;
|
||||
@@ -226,7 +232,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private boolean ignoreSelectionChanges=false;
|
||||
private Runnable updateUploadEtaRunnable;
|
||||
|
||||
private String language;
|
||||
private String language, encoding;
|
||||
private MastodonLanguage.LanguageResolver languageResolver;
|
||||
|
||||
@Override
|
||||
@@ -242,9 +248,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
instance=AccountSessionManager.getInstance().getInstanceInfo(instanceDomain);
|
||||
languageResolver=new MastodonLanguage.LanguageResolver(instance);
|
||||
redraftStatus=getArguments().getBoolean("redraftStatus", false);
|
||||
if(getArguments().containsKey("editStatus")){
|
||||
if(getArguments().containsKey("editStatus"))
|
||||
editingStatus=Parcels.unwrap(getArguments().getParcelable("editStatus"));
|
||||
}
|
||||
if(getArguments().containsKey("replyTo"))
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
|
||||
if(instance==null){
|
||||
Nav.finish(this);
|
||||
return;
|
||||
@@ -302,6 +309,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
selfName=view.findViewById(R.id.self_name);
|
||||
selfUsername=view.findViewById(R.id.self_username);
|
||||
selfAvatar=view.findViewById(R.id.self_avatar);
|
||||
selfExtraText=view.findViewById(R.id.self_extra_text);
|
||||
HtmlParser.setTextWithCustomEmoji(selfName, self.displayName, self.emojis);
|
||||
selfUsername.setText('@'+self.username+'@'+instanceDomain);
|
||||
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
|
||||
@@ -343,6 +351,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
pollBtn.setOnClickListener(v->togglePoll());
|
||||
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
||||
spoilerBtn.setOnClickListener(v->toggleSpoiler());
|
||||
|
||||
localOnly = savedInstanceState != null ? savedInstanceState.getBoolean("localOnly") :
|
||||
editingStatus != null ? editingStatus.localOnly : replyTo != null && replyTo.localOnly;
|
||||
|
||||
buildVisibilityPopup(visibilityBtn);
|
||||
visibilityBtn.setOnClickListener(v->visibilityPopup.show());
|
||||
visibilityBtn.setOnTouchListener(visibilityPopup.getDragToOpenListener());
|
||||
@@ -461,7 +473,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
case UNLISTED -> R.id.vis_unlisted;
|
||||
case PRIVATE -> R.id.vis_followers;
|
||||
case DIRECT -> R.id.vis_private;
|
||||
case LOCAL -> R.id.vis_local;
|
||||
}).setChecked(true);
|
||||
visibilityPopup.getMenu().findItem(R.id.local_only).setChecked(localOnly);
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
|
||||
@@ -488,6 +502,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
outState.putBoolean("pollAllowMultiple", pollAllowMultipleItem.isSelected());
|
||||
}
|
||||
outState.putBoolean("sensitive", sensitive);
|
||||
outState.putBoolean("localOnly", localOnly);
|
||||
outState.putBoolean("hasSpoiler", hasSpoiler);
|
||||
outState.putString("language", language);
|
||||
if(!attachments.isEmpty()){
|
||||
@@ -611,6 +626,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
});
|
||||
View originalPost = view.findViewById(R.id.original_post);
|
||||
extraText = view.findViewById(R.id.extra_text);
|
||||
originalPost.setVisibility(View.VISIBLE);
|
||||
originalPost.setOnClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
@@ -643,9 +659,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
view.findViewById(R.id.visibility).setVisibility(View.GONE);
|
||||
Drawable visibilityIcon = getActivity().getDrawable(switch(replyTo.visibility){
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
|
||||
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
|
||||
case LOCAL -> R.drawable.ic_fluent_eye_20_regular;
|
||||
});
|
||||
ImageView moreBtn = view.findViewById(R.id.more);
|
||||
moreBtn.setImageDrawable(visibilityIcon);
|
||||
@@ -669,6 +686,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
case UNLISTED -> R.string.sk_visibility_unlisted;
|
||||
case PRIVATE -> R.string.visibility_followers_only;
|
||||
case DIRECT -> R.string.visibility_private;
|
||||
case LOCAL -> R.string.sk_local_only;
|
||||
};
|
||||
replyText.setContentDescription(getString(R.string.in_reply_to, replyTo.account.displayName) + ". " + getString(R.string.post_visibility) + ": " + getString(visibilityNameRes));
|
||||
replyText.setOnClickListener(v->{
|
||||
@@ -695,7 +713,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(!TextUtils.isEmpty(replyTo.spoilerText)){
|
||||
hasSpoiler=true;
|
||||
spoilerEdit.setVisibility(View.VISIBLE);
|
||||
if(GlobalUserPreferences.prefixRepliesWithRe && !replyTo.spoilerText.startsWith("re: ")){
|
||||
spoilerEdit.setText("re: " + replyTo.spoilerText);
|
||||
}else{
|
||||
spoilerEdit.setText(replyTo.spoilerText);
|
||||
}
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
if (replyTo.language != null && !replyTo.language.isEmpty()) updateLanguage(replyTo.language);
|
||||
@@ -749,6 +771,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
updateSensitive();
|
||||
updateHeaders();
|
||||
|
||||
if(editingStatus!=null){
|
||||
updateCharCounter();
|
||||
@@ -819,9 +842,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void updateLanguage(MastodonLanguage loc) {
|
||||
language = loc.getLanguage();
|
||||
languageButton.setText(loc.getLanguageName());
|
||||
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, loc.getDefaultName()));
|
||||
updateLanguage(loc.getLanguage(), loc.getLanguageName(), loc.getDefaultName());
|
||||
}
|
||||
|
||||
private void updateLanguage(String languageTag, String languageName, String defaultName) {
|
||||
language = languageTag;
|
||||
languageButton.setText(languageName);
|
||||
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, defaultName));
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -838,9 +865,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
Menu languageMenu = languagePopup.getMenu();
|
||||
for (String recentLanguage : Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages)) {
|
||||
if (recentLanguage.equals("bottom")) {
|
||||
addBottomLanguage(languageMenu);
|
||||
} else {
|
||||
MastodonLanguage l = languageResolver.from(recentLanguage);
|
||||
languageMenu.add(0, allLanguages.indexOf(l), Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
|
||||
}
|
||||
}
|
||||
|
||||
SubMenu allLanguagesMenu = languageMenu.addSubMenu(R.string.sk_available_languages);
|
||||
for (int i = 0; i < allLanguages.size(); i++) {
|
||||
@@ -848,13 +879,33 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
allLanguagesMenu.add(0, i, Menu.NONE, getActivity().getString(R.string.sk_language_name, l.getDefaultName(), l.getLanguageName()));
|
||||
}
|
||||
|
||||
if (GlobalUserPreferences.bottomEncoding) addBottomLanguage(allLanguagesMenu);
|
||||
|
||||
btn.setOnLongClickListener(v->{
|
||||
btn.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
if (!GlobalUserPreferences.bottomEncoding) addBottomLanguage(allLanguagesMenu);
|
||||
return false;
|
||||
});
|
||||
|
||||
languagePopup.setOnMenuItemClickListener(i->{
|
||||
if (i.hasSubMenu()) return false;
|
||||
if (i.getItemId() == allLanguages.size()) {
|
||||
updateLanguage(language, "\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48", "bottom");
|
||||
encoding = "bottom";
|
||||
} else {
|
||||
updateLanguage(allLanguages.get(i.getItemId()));
|
||||
encoding = null;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void addBottomLanguage(Menu menu) {
|
||||
if (menu.findItem(allLanguages.size()) == null) {
|
||||
menu.add(0, allLanguages.size(), Menu.NONE, "bottom (\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48)");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
return true;
|
||||
@@ -884,6 +935,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(hasSpoiler){
|
||||
charCount+=spoilerEdit.length();
|
||||
}
|
||||
if (localOnly && GlobalUserPreferences.accountsInGlitchMode.contains(accountID)) {
|
||||
charCount -= GLITCH_LOCAL_ONLY_SUFFIX.length();
|
||||
}
|
||||
charCounter.setText(String.valueOf(charLimit-charCount));
|
||||
trimmedCharCount=text.toString().trim().length();
|
||||
updatePublishButtonState();
|
||||
@@ -976,8 +1030,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private void publish(boolean force){
|
||||
String text=mainEditText.getText().toString();
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
if ("bottom".equals(encoding)) {
|
||||
text = new StatusTextEncoder(Bottom::encode).encode(text);
|
||||
req.spoilerText = "bottom-encoded emoji spam";
|
||||
}
|
||||
if (localOnly &&
|
||||
GlobalUserPreferences.accountsInGlitchMode.contains(accountID) &&
|
||||
!GLITCH_LOCAL_ONLY_PATTERN.matcher(text).matches()) {
|
||||
text += " " + GLITCH_LOCAL_ONLY_SUFFIX;
|
||||
}
|
||||
req.status=text;
|
||||
req.visibility=statusVisibility;
|
||||
req.localOnly=localOnly;
|
||||
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
|
||||
req.sensitive=sensitive;
|
||||
req.language=language;
|
||||
req.scheduledAt = scheduledAt;
|
||||
@@ -1111,6 +1175,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
List<String> newRecentLanguages = new ArrayList<>(Optional.ofNullable(recentLanguages.get(accountID)).orElse(defaultRecentLanguages));
|
||||
newRecentLanguages.remove(language);
|
||||
newRecentLanguages.add(0, language);
|
||||
if (encoding != null) {
|
||||
newRecentLanguages.remove(encoding);
|
||||
newRecentLanguages.add(0, encoding);
|
||||
}
|
||||
if ("bottom".equals(encoding) && !GlobalUserPreferences.bottomEncoding) {
|
||||
GlobalUserPreferences.bottomEncoding = true;
|
||||
GlobalUserPreferences.save();
|
||||
}
|
||||
recentLanguages.put(accountID, newRecentLanguages.stream().limit(4).collect(Collectors.toList()));
|
||||
GlobalUserPreferences.save();
|
||||
}
|
||||
@@ -1176,7 +1248,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void confirmDiscardDraftAndFinish(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
boolean attachmentsPending = attachments.stream().anyMatch(att -> att.state != AttachmentUploadState.DONE);
|
||||
if (attachmentsPending) new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_unfinished_attachments)
|
||||
.setMessage(R.string.sk_unfinished_attachments_message)
|
||||
.setPositiveButton(R.string.edit, (d, w) -> {})
|
||||
.setNegativeButton(R.string.discard, (d, w) -> Nav.finish(this))
|
||||
.show();
|
||||
else new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(editingStatus != null ? R.string.sk_confirm_save_changes : R.string.sk_confirm_save_draft)
|
||||
.setPositiveButton(R.string.save, (d, w) -> {
|
||||
updateScheduledAt(scheduledAt == null ? getDraftInstant() : scheduledAt);
|
||||
@@ -1186,18 +1265,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if Android platform photopicker is available on the device\
|
||||
* @return whether the device supports photopicker intents.
|
||||
*/
|
||||
private boolean isPhotoPickerAvailable() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return true;
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return getExtensionVersion(Build.VERSION_CODES.R) >= 2;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the correct intent for the device version to select media.
|
||||
@@ -1212,7 +1279,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
boolean usePhotoPicker=photoPicker && isPhotoPickerAvailable();
|
||||
if(usePhotoPicker){
|
||||
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MAX_ATTACHMENTS-getMediaAttachmentsCount());
|
||||
}else{
|
||||
intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
@@ -1758,12 +1825,33 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
return attachments.size();
|
||||
}
|
||||
|
||||
private void updateHeaders() {
|
||||
UiUtils.setExtraTextInfo(getContext(), selfExtraText, statusVisibility, localOnly);
|
||||
if (replyTo != null) UiUtils.setExtraTextInfo(getContext(), extraText, replyTo.visibility, replyTo.localOnly);
|
||||
}
|
||||
|
||||
private void buildVisibilityPopup(View v){
|
||||
visibilityPopup=new PopupMenu(getActivity(), v);
|
||||
visibilityPopup.inflate(R.menu.compose_visibility);
|
||||
Menu m=visibilityPopup.getMenu();
|
||||
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
|
||||
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
||||
if (instance.pleroma != null) {
|
||||
m.findItem(R.id.vis_local).setVisible(true);
|
||||
} else if (localOnly || prefsSaysSupported) {
|
||||
localOnlyItem.setVisible(true);
|
||||
localOnlyItem.setChecked(localOnly);
|
||||
Status status = editingStatus != null ? editingStatus : replyTo;
|
||||
if (!prefsSaysSupported) {
|
||||
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
|
||||
if (GLITCH_LOCAL_ONLY_PATTERN.matcher(status.getStrippedText()).matches()) {
|
||||
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
||||
}
|
||||
GlobalUserPreferences.save();
|
||||
}
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), visibilityPopup);
|
||||
m.setGroupCheckable(0, true, true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) m.setGroupDividerEnabled(true);
|
||||
visibilityPopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item){
|
||||
@@ -1776,19 +1864,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
statusVisibility=StatusPrivacy.PRIVATE;
|
||||
}else if(id==R.id.vis_private){
|
||||
statusVisibility=StatusPrivacy.DIRECT;
|
||||
}else if(id==R.id.vis_local){
|
||||
statusVisibility=StatusPrivacy.LOCAL;
|
||||
}
|
||||
if (id == R.id.local_only) {
|
||||
localOnly = !item.isChecked();
|
||||
item.setChecked(localOnly);
|
||||
} else {
|
||||
item.setChecked(true);
|
||||
}
|
||||
updateVisibilityIcon();
|
||||
updateHeaders();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadDefaultStatusVisibility(Bundle savedInstanceState) {
|
||||
if(getArguments().containsKey("replyTo")){
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
|
||||
statusVisibility = replyTo.visibility;
|
||||
}
|
||||
if(replyTo != null) statusVisibility = replyTo.visibility;
|
||||
|
||||
// A saved privacy setting from a previous compose session wins over the reply visibility
|
||||
if(savedInstanceState !=null){
|
||||
@@ -1797,17 +1890,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
AccountSessionManager asm = AccountSessionManager.getInstance();
|
||||
Preferences prefs = asm.getAccount(accountID).preferences;
|
||||
if (prefs != null && replyTo != null) {
|
||||
if (prefs != null) {
|
||||
// Only override the reply visibility if our preference is more private
|
||||
// (or we're replying to ourselves)
|
||||
// (and we're not replying to ourselves, or not at all)
|
||||
if (prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility) &&
|
||||
!asm.isSelf(accountID, replyTo.account)) {
|
||||
statusVisibility = switch (prefs.postingDefaultVisibility) {
|
||||
case PUBLIC -> StatusPrivacy.PUBLIC;
|
||||
case UNLISTED -> StatusPrivacy.UNLISTED;
|
||||
case PRIVATE -> StatusPrivacy.PRIVATE;
|
||||
case DIRECT -> StatusPrivacy.DIRECT;
|
||||
};
|
||||
(replyTo == null || !asm.isSelf(accountID, replyTo.account))) {
|
||||
statusVisibility = prefs.postingDefaultVisibility;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1823,9 +1911,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
visibilityBtn.setImageResource(switch(statusVisibility){
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_24_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_24_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_24_filled;
|
||||
case DIRECT -> R.drawable.ic_fluent_mention_24_regular;
|
||||
case LOCAL -> R.drawable.ic_fluent_eye_24_regular;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ public class EditTimelinesFragment extends BaseRecyclerFragment<TimelineDefiniti
|
||||
}
|
||||
|
||||
private void updateOptionsMenu() {
|
||||
if (getActivity() == null) return;
|
||||
optionsMenu.clear();
|
||||
timelineByMenuItem.clear();
|
||||
|
||||
@@ -156,7 +157,7 @@ public class EditTimelinesFragment extends BaseRecyclerFragment<TimelineDefiniti
|
||||
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
|
||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_list_24_regular);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
|
||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public abstract class FabStatusListFragment extends StatusListFragment {
|
||||
protected ImageButton fab;
|
||||
|
||||
public FabStatusListFragment() {
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab = view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(this::onFabLongClick);
|
||||
}
|
||||
|
||||
protected void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
protected boolean onFabLongClick(View v) {
|
||||
return UiUtils.pickAccountForCompose(getActivity(), accountID);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public class FavoritedStatusListFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
|
||||
@@ -80,6 +80,7 @@ public class FollowRequestsListFragment extends BaseRecyclerFragment<FollowReque
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
if (getActivity() == null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
|
||||
@@ -55,6 +55,7 @@ public class FollowedHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
if (getActivity() == null) return;
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
|
||||
@@ -17,12 +17,15 @@ import org.joinmastodon.android.api.requests.tags.GetHashtag;
|
||||
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
|
||||
import org.joinmastodon.android.events.HashtagUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -33,11 +36,11 @@ import me.grishka.appkit.utils.V;
|
||||
public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
private String hashtag;
|
||||
private boolean following;
|
||||
private ImageButton fab;
|
||||
private MenuItem followButton;
|
||||
|
||||
public HashtagTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,6 +73,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
new GetHashtag(hashtag).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag hashtag) {
|
||||
if (getActivity() == null) return;
|
||||
updateTitle(hashtag.name);
|
||||
updateFollowingState(hashtag.following);
|
||||
}
|
||||
@@ -91,6 +95,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
new SetHashtagFollowed(hashtag, following).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Hashtag i) {
|
||||
if (getActivity() == null) return;
|
||||
if (i.following == following) Toast.makeText(getActivity(), getString(i.following ? R.string.followed_user : R.string.unfollowed_user, "#" + i.name), Toast.LENGTH_SHORT).show();
|
||||
updateFollowingState(i.following);
|
||||
}
|
||||
@@ -117,6 +122,8 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -131,14 +138,12 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' '));
|
||||
protected boolean onFabLongClick(View v) {
|
||||
return UiUtils.pickAccountForCompose(getActivity(), accountID, '#'+hashtag+' ');
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
@Override
|
||||
protected void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putString("prefilledText", '#'+hashtag+' ');
|
||||
|
||||
@@ -16,12 +16,10 @@ import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.PushNotificationReceiver;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.discover.SearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
@@ -95,8 +95,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
private TimelineDefinition[] timelines;
|
||||
private final Map<Integer, TimelineDefinition> timelinesByMenuItem = new HashMap<>();
|
||||
private SubMenu hashtagsMenu, listsMenu;
|
||||
private Menu optionsMenu;
|
||||
private MenuInflater optionsMenuInflater;
|
||||
private PopupMenu overflowPopup;
|
||||
private View overflowActionView = null;
|
||||
private boolean announcementsBadged, settingsBadged;
|
||||
|
||||
@Override
|
||||
@@ -153,6 +153,12 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
|
||||
view.addView(pager, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
overflowActionView = UiUtils.makeOverflowActionView(getContext());
|
||||
overflowPopup = new PopupMenu(getContext(), overflowActionView);
|
||||
overflowPopup.setOnMenuItemClickListener(this::onOptionsItemSelected);
|
||||
overflowActionView.setOnClickListener(l -> overflowPopup.show());
|
||||
overflowActionView.setOnTouchListener(overflowPopup.getDragToOpenListener());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -231,21 +237,62 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
updateUpdateState(GithubSelfUpdater.getInstance().getState());
|
||||
}
|
||||
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
updateList(lists, listItems);
|
||||
}
|
||||
|
||||
private void addListsToOptionsMenu() {
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetFollowedHashtags().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> hashtags) {
|
||||
updateList(hashtags, hashtagsItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetAnnouncements(false).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Announcement> result) {
|
||||
if (getActivity() == null) return;
|
||||
if (result.stream().anyMatch(a -> !a.read)) {
|
||||
announcementsBadged = true;
|
||||
announcements.setVisible(false);
|
||||
announcementsAction.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
private void addListsToOverflowMenu() {
|
||||
Context ctx = getContext();
|
||||
listsMenu.clear();
|
||||
listsMenu.getItem().setVisible(listItems.size() > 0);
|
||||
UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(listsMenu));
|
||||
listItems.forEach((id, list) -> {
|
||||
MenuItem item = listsMenu.add(Menu.NONE, id, Menu.NONE, list.title);
|
||||
item.setIcon(R.drawable.ic_fluent_people_list_24_regular);
|
||||
item.setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||
UiUtils.insetPopupMenuIcon(ctx, item);
|
||||
});
|
||||
}
|
||||
|
||||
private void addHashtagsToOptionsMenu() {
|
||||
private void addHashtagsToOverflowMenu() {
|
||||
Context ctx = getContext();
|
||||
hashtagsMenu.clear();
|
||||
hashtagsMenu.getItem().setVisible(hashtagsItems.size() > 0);
|
||||
@@ -291,79 +338,46 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
}
|
||||
}
|
||||
|
||||
private void createOptionsMenu() {
|
||||
optionsMenu.clear();
|
||||
optionsMenuInflater.inflate(R.menu.home, optionsMenu);
|
||||
announcements = optionsMenu.findItem(R.id.announcements);
|
||||
announcementsAction = optionsMenu.findItem(R.id.announcements_action);
|
||||
settings = optionsMenu.findItem(R.id.settings);
|
||||
settingsAction = optionsMenu.findItem(R.id.settings_action);
|
||||
hashtagsMenu = optionsMenu.findItem(R.id.hashtags).getSubMenu();
|
||||
listsMenu = optionsMenu.findItem(R.id.lists).getSubMenu();
|
||||
private void updateOverflowMenu() {
|
||||
if (getActivity() == null) return;
|
||||
Menu m = overflowPopup.getMenu();
|
||||
m.clear();
|
||||
overflowPopup.inflate(R.menu.home_overflow);
|
||||
announcements = m.findItem(R.id.announcements);
|
||||
settings = m.findItem(R.id.settings);
|
||||
hashtagsMenu = m.findItem(R.id.hashtags).getSubMenu();
|
||||
listsMenu = m.findItem(R.id.lists).getSubMenu();
|
||||
|
||||
announcements.setVisible(!announcementsBadged);
|
||||
announcementsAction.setVisible(announcementsBadged);
|
||||
settings.setVisible(!settingsBadged);
|
||||
settingsAction.setVisible(settingsBadged);
|
||||
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu,
|
||||
R.id.overflow, R.id.announcements_action, R.id.settings_action);
|
||||
UiUtils.enablePopupMenuIcons(getContext(), overflowPopup);
|
||||
|
||||
addListsToOptionsMenu();
|
||||
addHashtagsToOptionsMenu();
|
||||
addListsToOverflowMenu();
|
||||
addHashtagsToOverflowMenu();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
m.setGroupDividerEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
this.optionsMenu = menu;
|
||||
this.optionsMenuInflater = inflater;
|
||||
createOptionsMenu();
|
||||
inflater.inflate(R.menu.home, menu);
|
||||
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
updateList(lists, listItems);
|
||||
}
|
||||
menu.findItem(R.id.overflow).setActionView(overflowActionView);
|
||||
announcementsAction = menu.findItem(R.id.announcements_action);
|
||||
settingsAction = menu.findItem(R.id.settings_action);
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetFollowedHashtags().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> hashtags) {
|
||||
updateList(hashtags, hashtagsItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetAnnouncements(false).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<Announcement> result) {
|
||||
if (result.stream().anyMatch(a -> !a.read)) {
|
||||
announcementsBadged = true;
|
||||
announcements.setVisible(false);
|
||||
announcementsAction.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
}).exec(accountID);
|
||||
updateOverflowMenu();
|
||||
}
|
||||
|
||||
private <T> void updateList(List<T> addItems, Map<Integer, T> items) {
|
||||
if (addItems.size() == 0) return;
|
||||
if (addItems.size() == 0 || getActivity() == null) return;
|
||||
for (int i = 0; i < addItems.size(); i++) items.put(View.generateViewId(), addItems.get(i));
|
||||
createOptionsMenu();
|
||||
updateOverflowMenu();
|
||||
}
|
||||
|
||||
private void updateSwitcherMenu() {
|
||||
@@ -427,8 +441,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
Hashtag hashtag;
|
||||
|
||||
if (item.getItemId() == R.id.menu_back) {
|
||||
createOptionsMenu();
|
||||
optionsMenu.performIdentifierAction(R.id.overflow, 0);
|
||||
getToolbar().post(() -> overflowPopup.show());
|
||||
return true;
|
||||
} else if (id == R.id.settings || id == R.id.settings_action) {
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
@@ -563,6 +576,14 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if (overflowPopup != null) {
|
||||
overflowPopup.dismiss();
|
||||
overflowPopup = null;
|
||||
}
|
||||
if (switcherPopup != null) {
|
||||
switcherPopup.dismiss();
|
||||
switcherPopup = null;
|
||||
}
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
E.unregister(this);
|
||||
}
|
||||
@@ -625,10 +646,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
if (shouldBeInList) {
|
||||
existingThings.put(existingThing.isPresent()
|
||||
? existingThing.get().getKey() : View.generateViewId(), makeNewThing.get());
|
||||
createOptionsMenu();
|
||||
updateOverflowMenu();
|
||||
} else if (existingThing.isPresent() && !shouldBeInList) {
|
||||
existingThings.remove(existingThing.get().getKey());
|
||||
createOptionsMenu();
|
||||
updateOverflowMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,7 +662,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
@Override
|
||||
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
FrameLayout tabView = tabViews[viewType % getItemCount()];
|
||||
((ViewGroup)tabView.getParent()).removeView(tabView);
|
||||
ViewGroup tabParent = (ViewGroup) tabView.getParent();
|
||||
if (tabParent != null) tabParent.removeView(tabView);
|
||||
tabView.setVisibility(View.VISIBLE);
|
||||
return new SimpleViewHolder(tabView);
|
||||
}
|
||||
|
||||
@@ -32,11 +32,16 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTimelineFragment extends FabStatusListFragment {
|
||||
public class HomeTimelineFragment extends StatusListFragment {
|
||||
private HomeTabFragment parent;
|
||||
private String maxID;
|
||||
private String lastSavedMarkerID;
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
@@ -58,8 +63,7 @@ public class HomeTimelineFragment extends FabStatusListFragment {
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if (getActivity() == null) return;
|
||||
List<Status> filteredItems = filterPosts(result.items);
|
||||
onDataLoaded(filteredItems, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
@@ -150,7 +154,7 @@ public class HomeTimelineFragment extends FabStatusListFragment {
|
||||
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
if (parent != null) parent.showNewPostsButton();
|
||||
if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,17 @@ import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ListTimelineEditor;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -39,10 +42,10 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
private String listTitle;
|
||||
@Nullable
|
||||
private ListTimeline.RepliesPolicy repliesPolicy;
|
||||
private ImageButton fab;
|
||||
|
||||
public ListTimelineFragment() {
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,6 +62,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
new GetList(listID).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline listTimeline) {
|
||||
if (getActivity() == null) return;
|
||||
// TODO: save updated info
|
||||
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
|
||||
if (listTimeline.repliesPolicy != null && !listTimeline.repliesPolicy.equals(repliesPolicy)) {
|
||||
@@ -88,7 +92,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
editor.applyList(listTitle, repliesPolicy);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.sk_edit_list_title)
|
||||
.setIcon(R.drawable.ic_fluent_people_list_28_regular)
|
||||
.setIcon(R.drawable.ic_fluent_people_28_regular)
|
||||
.setView(editor)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
String newTitle = editor.getTitle().trim();
|
||||
@@ -96,6 +100,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
new UpdateList(listID, newTitle, editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
if (getActivity() == null) return;
|
||||
setTitle(list.title);
|
||||
listTitle = list.title;
|
||||
repliesPolicy = list.repliesPolicy;
|
||||
@@ -131,6 +136,8 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<Status> result) {
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
@@ -145,14 +152,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID));
|
||||
}
|
||||
|
||||
private void onFabClick(View v){
|
||||
protected void onFabClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
|
||||
@@ -99,7 +99,6 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
new CreateList(editor.getTitle(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
saveListMembership(list.id, true);
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.repliesPolicy));
|
||||
@@ -140,6 +139,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
if (getActivity() == null) return;
|
||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
||||
userInList.putAll(userInListBefore);
|
||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||
@@ -148,6 +148,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListTimelinesFragment.this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> allLists) {
|
||||
if (getActivity() == null) return;
|
||||
List<ListTimeline> newLists = new ArrayList<>();
|
||||
for (ListTimeline l : allLists) {
|
||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||
@@ -230,7 +231,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment<ListTimeline> im
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_list_24_regular), null, null, null);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(R.drawable.ic_fluent_people_24_regular), null, null, null);
|
||||
if (profileAccountId != null) {
|
||||
Boolean checked = userInList.get(item.id);
|
||||
listToggle.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -187,6 +187,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
|
||||
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> accounts) {
|
||||
if (getActivity() == null) return;
|
||||
getToolbar().getMenu().findItem(R.id.follow_requests).setVisible(!accounts.isEmpty());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -12,15 +13,20 @@ import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -42,6 +48,11 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
private String maxID;
|
||||
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS);
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -71,6 +82,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null :
|
||||
n.report.targetAccount;
|
||||
String extraText=switch(n.type){
|
||||
case FOLLOW -> getString(R.string.user_followed_you);
|
||||
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
|
||||
@@ -79,10 +92,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
case FAVORITE -> getString(R.string.user_favorited);
|
||||
case POLL -> getString(R.string.poll_ended);
|
||||
case UPDATE -> getString(R.string.sk_post_edited);
|
||||
case SIGN_UP -> getString(R.string.sk_signed_up);
|
||||
case REPORT -> getString(R.string.sk_reported);
|
||||
};
|
||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText, n, null) : null;
|
||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, extraText, n, null) : null;
|
||||
if(n.status!=null){
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n);
|
||||
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS);
|
||||
if(titleItem!=null){
|
||||
for(StatusDisplayItem item:items){
|
||||
if(item instanceof ImageStatusDisplayItem imgItem){
|
||||
@@ -94,8 +109,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
items.add(0, titleItem);
|
||||
return items;
|
||||
}else if(titleItem!=null){
|
||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account, n);
|
||||
return Arrays.asList(titleItem, card);
|
||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this,
|
||||
reportTarget != null ? reportTarget : n.account, n);
|
||||
TextStatusDisplayItem text = n.report != null && !TextUtils.isEmpty(n.report.comment) ?
|
||||
new TextStatusDisplayItem(n.id, n.report.comment, this,
|
||||
Status.ofFake(n.id, n.report.comment, n.createdAt), true) :
|
||||
null;
|
||||
return text == null ? Arrays.asList(titleItem, card) : Arrays.asList(titleItem, text, card);
|
||||
}else{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -116,8 +136,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if (getActivity() == null) return;
|
||||
if(refreshing)
|
||||
relationships.clear();
|
||||
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
||||
@@ -164,6 +183,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}else if(n.report != null){
|
||||
String domain = AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://"+domain+"/admin/reports/"+n.report.id);
|
||||
}else{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -176,6 +198,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
if (getParentFragment() instanceof NotificationsFragment) fab.setVisibility(View.GONE);
|
||||
if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static android.content.Context.CLIPBOARD_SERVICE;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ImageSpan;
|
||||
@@ -34,15 +29,14 @@ import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
@@ -61,6 +55,7 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable;
|
||||
@@ -69,8 +64,10 @@ import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CoverImageView;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.parceler.Parcels;
|
||||
@@ -84,6 +81,9 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
@@ -94,10 +94,17 @@ import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{
|
||||
private static final int AVATAR_RESULT=722;
|
||||
@@ -105,23 +112,24 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
private ImageView avatar;
|
||||
private CoverImageView cover;
|
||||
private View avatarBorder;
|
||||
private View avatarBorder, nameWrap;
|
||||
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
|
||||
private ProgressBarButton actionButton, notifyButton;
|
||||
private ViewPager2 pager;
|
||||
private NestedRecyclerScrollView scrollView;
|
||||
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
|
||||
private ProfileAboutFragment aboutFragment;
|
||||
// private ProfileAboutFragment aboutFragment;
|
||||
private TabLayout tabbar;
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
private CoverOverlayGradientDrawable coverGradient=new CoverOverlayGradientDrawable();
|
||||
private float titleTransY;
|
||||
private View postsBtn, followersBtn, followingBtn;
|
||||
private View postsBtn, followersBtn, followingBtn, profileCounters;
|
||||
private EditText nameEdit, bioEdit;
|
||||
private ProgressBar actionProgress, notifyProgress;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private TextView followsYouView;
|
||||
private ViewGroup rolesView;
|
||||
|
||||
private Account account;
|
||||
private String accountID;
|
||||
@@ -139,6 +147,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private PhotoViewer currentPhotoViewer;
|
||||
private boolean editModeLoading;
|
||||
|
||||
private static final int MAX_FIELDS=4;
|
||||
|
||||
// from ProfileAboutFragment
|
||||
public UsableRecyclerView list;
|
||||
private List<AccountField> metadataListData=Collections.emptyList();
|
||||
private MetadataAdapter adapter;
|
||||
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
|
||||
private RecyclerView.ViewHolder draggedViewHolder;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
|
||||
public ProfileFragment(){
|
||||
super(R.layout.loader_fragment_overlay_toolbar);
|
||||
}
|
||||
@@ -183,8 +201,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
cover=content.findViewById(R.id.cover);
|
||||
avatarBorder=content.findViewById(R.id.avatar_border);
|
||||
name=content.findViewById(R.id.name);
|
||||
nameWrap=content.findViewById(R.id.name_wrap);
|
||||
username=content.findViewById(R.id.username);
|
||||
bio=content.findViewById(R.id.bio);
|
||||
profileCounters=content.findViewById(R.id.profile_counters);
|
||||
followersCount=content.findViewById(R.id.followers_count);
|
||||
followersLabel=content.findViewById(R.id.followers_label);
|
||||
followersBtn=content.findViewById(R.id.followers_btn);
|
||||
@@ -206,6 +226,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
notifyProgress=content.findViewById(R.id.notify_progress);
|
||||
fab=content.findViewById(R.id.fab);
|
||||
followsYouView=content.findViewById(R.id.follows_you);
|
||||
list=content.findViewById(R.id.metadata);
|
||||
rolesView=content.findViewById(R.id.roles);
|
||||
|
||||
avatar.setOutlineProvider(new ViewOutlineProvider(){
|
||||
@Override
|
||||
@@ -225,7 +247,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
};
|
||||
|
||||
tabViews=new FrameLayout[5];
|
||||
tabViews=new FrameLayout[4];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
@@ -242,7 +264,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
UiUtils.reduceSwipeSensitivity(pager);
|
||||
pager.setOffscreenPageLimit(5);
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
|
||||
pager.setAdapter(new ProfilePagerAdapter());
|
||||
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
|
||||
@@ -303,6 +325,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return true;
|
||||
});
|
||||
|
||||
// from ProfileAboutFragment
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.setDrawSelectorOnTop(true);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
|
||||
list.setAdapter(adapter=new MetadataAdapter());
|
||||
list.setClipToPadding(false);
|
||||
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
@@ -312,6 +342,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
if (getActivity() == null) return;
|
||||
account=result;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
bindHeaderView();
|
||||
@@ -357,8 +388,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
|
||||
pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
|
||||
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
|
||||
aboutFragment=new ProfileAboutFragment();
|
||||
aboutFragment.setFields(fields);
|
||||
// aboutFragment=new ProfileAboutFragment();
|
||||
setFields(fields);
|
||||
}
|
||||
pager.getAdapter().notifyDataSetChanged();
|
||||
super.dataLoaded();
|
||||
@@ -451,6 +482,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
|
||||
if (account.roles != null && !account.roles.isEmpty()) {
|
||||
rolesView.setVisibility(View.VISIBLE);
|
||||
rolesView.removeAllViews();
|
||||
name.setPadding(0, 0, V.dp(12), 0);
|
||||
for (Account.Role role : account.roles) {
|
||||
TextView roleText = new TextView(getActivity(), null, 0, R.style.role_label);
|
||||
roleText.setText(role.name);
|
||||
GradientDrawable bg = (GradientDrawable) roleText.getBackground().mutate();
|
||||
bg.setStroke(V.dp(2), Color.parseColor(role.color));
|
||||
rolesView.addView(roleText);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
|
||||
if(account.locked){
|
||||
@@ -518,9 +562,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
fields.add(field);
|
||||
}
|
||||
|
||||
if(aboutFragment!=null){
|
||||
aboutFragment.setFields(fields);
|
||||
}
|
||||
setFields(fields);
|
||||
}
|
||||
|
||||
private void updateToolbar(){
|
||||
@@ -679,6 +721,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
private void updateRelationship(){
|
||||
if (getActivity() == null) return;
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
|
||||
@@ -688,7 +731,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
|
||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||
notifyButton.setSelected(relationship.notifying);
|
||||
if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
|
||||
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
|
||||
}
|
||||
|
||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
@@ -711,8 +754,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
coverGradient.setTopOffset(scrollY);
|
||||
cover.invalidate();
|
||||
titleTransY=getToolbar().getHeight();
|
||||
if(scrollY>name.getTop()-topBarsH){
|
||||
titleTransY=Math.max(0f, titleTransY-(scrollY-(name.getTop()-topBarsH)));
|
||||
if(scrollY>nameWrap.getTop()-topBarsH){
|
||||
titleTransY=Math.max(0f, titleTransY-(scrollY-(nameWrap.getTop()-topBarsH)));
|
||||
}
|
||||
if(toolbarTitleView!=null){
|
||||
toolbarTitleView.setTranslationY(titleTransY);
|
||||
@@ -729,7 +772,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
case 1 -> postsWithRepliesFragment;
|
||||
case 2 -> pinnedPostsFragment;
|
||||
case 3 -> mediaFragment;
|
||||
case 4 -> aboutFragment;
|
||||
// case 4 -> aboutFragment;
|
||||
default -> throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
@@ -771,8 +814,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
editModeLoading=false;
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if (getActivity() == null) return;
|
||||
enterEditMode(result);
|
||||
setActionProgressVisible(false);
|
||||
}
|
||||
@@ -780,8 +822,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
editModeLoading=false;
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if (getActivity() == null) return;
|
||||
error.showToast(getActivity());
|
||||
setActionProgressVisible(false);
|
||||
}
|
||||
@@ -796,16 +837,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
invalidateOptionsMenu();
|
||||
pager.setUserInputEnabled(false);
|
||||
actionButton.setText(R.string.done);
|
||||
pager.setCurrentItem(4);
|
||||
ArrayList<Animator> animators=new ArrayList<>();
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
|
||||
tabbar.getTabAt(i).view.setEnabled(false);
|
||||
}
|
||||
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate();
|
||||
avatar.setForeground(overlay);
|
||||
animators.add(ObjectAnimator.ofInt(overlay, "alpha", 0, 255));
|
||||
|
||||
nameWrap.setVisibility(View.GONE);
|
||||
nameEdit.setVisibility(View.VISIBLE);
|
||||
nameEdit.setText(account.displayName);
|
||||
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams();
|
||||
@@ -817,10 +854,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
bioEdit.setText(account.source.note);
|
||||
animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f, 1f));
|
||||
animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 0f));
|
||||
|
||||
animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, .3f));
|
||||
animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, .3f));
|
||||
animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, .3f));
|
||||
profileCounters.setVisibility(View.GONE);
|
||||
pager.setVisibility(View.GONE);
|
||||
tabbar.setVisibility(View.GONE);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(animators);
|
||||
@@ -828,7 +864,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.start();
|
||||
|
||||
aboutFragment.enterEditMode(account.source.fields);
|
||||
// aboutFragment.enterEditMode(account.source.fields);
|
||||
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
metadataListData=account.source.fields;
|
||||
adapter.notifyDataSetChanged();
|
||||
dragHelper.attachToRecyclerView(list);
|
||||
}
|
||||
|
||||
private void exitEditMode(){
|
||||
@@ -839,16 +880,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
invalidateOptionsMenu();
|
||||
ArrayList<Animator> animators=new ArrayList<>();
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
|
||||
}
|
||||
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
|
||||
animators.add(ObjectAnimator.ofFloat(nameEdit, View.ALPHA, 0f));
|
||||
animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f));
|
||||
animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 1f));
|
||||
animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, 1f));
|
||||
animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, 1f));
|
||||
animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, 1f));
|
||||
profileCounters.setVisibility(View.VISIBLE);
|
||||
pager.setVisibility(View.VISIBLE);
|
||||
tabbar.setVisibility(View.VISIBLE);
|
||||
V.setVisibilityAnimated(nameWrap, View.VISIBLE);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(animators);
|
||||
@@ -857,20 +896,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
for(int i=0;i<tabViews.length-1;i++){
|
||||
tabbar.getTabAt(i).view.setEnabled(true);
|
||||
}
|
||||
pager.setUserInputEnabled(true);
|
||||
nameEdit.setVisibility(View.GONE);
|
||||
bioEdit.setVisibility(View.GONE);
|
||||
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams();
|
||||
lp.addRule(RelativeLayout.BELOW, R.id.name);
|
||||
lp.addRule(RelativeLayout.BELOW, R.id.name_wrap);
|
||||
username.getParent().requestLayout();
|
||||
avatar.setForeground(null);
|
||||
scrollToTop();
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(content.getWindowToken(), 0);
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
bindHeaderView();
|
||||
}
|
||||
|
||||
@@ -878,12 +918,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(!isInEditMode)
|
||||
throw new IllegalStateException();
|
||||
setActionProgressVisible(true);
|
||||
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), editNewAvatar, editNewCover, aboutFragment.getFields())
|
||||
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), editNewAvatar, editNewCover, metadataListData)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
account=result;
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, account);
|
||||
if (getActivity() == null) return;
|
||||
exitEditMode();
|
||||
setActionProgressVisible(false);
|
||||
}
|
||||
@@ -1048,4 +1089,227 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
// from ProfileAboutFragment
|
||||
public void setFields(ArrayList<AccountField> fields){
|
||||
metadataListData=fields;
|
||||
if (isInEditMode) {
|
||||
isInEditMode=false;
|
||||
dragHelper.attachToRecyclerView(null);
|
||||
}
|
||||
if (adapter != null) adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private class MetadataAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter {
|
||||
public MetadataAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return switch(viewType){
|
||||
case 0 -> new AboutViewHolder();
|
||||
case 1 -> new EditableAboutViewHolder();
|
||||
case 2 -> new AddRowViewHolder();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(BaseViewHolder holder, int position){
|
||||
if(position<metadataListData.size()){
|
||||
holder.bind(metadataListData.get(position));
|
||||
}else{
|
||||
holder.bind(null);
|
||||
}
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
if(isInEditMode){
|
||||
int size=metadataListData.size();
|
||||
if(size<MAX_FIELDS)
|
||||
size++;
|
||||
return size;
|
||||
}
|
||||
return metadataListData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
if(isInEditMode){
|
||||
return position==metadataListData.size() ? 2 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return isInEditMode || metadataListData.get(position).emojiRequests==null
|
||||
? 0 : metadataListData.get(position).emojiRequests.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
return metadataListData.get(position).emojiRequests.get(image);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class BaseViewHolder extends BindableViewHolder<AccountField> {
|
||||
public BaseViewHolder(int layout){
|
||||
super(getActivity(), layout, list);
|
||||
}
|
||||
}
|
||||
|
||||
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder {
|
||||
private TextView title;
|
||||
private LinkedTextView value;
|
||||
|
||||
public AboutViewHolder(){
|
||||
super(R.layout.item_profile_about);
|
||||
title=findViewById(R.id.title);
|
||||
value=findViewById(R.id.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountField item){
|
||||
title.setText(item.parsedName);
|
||||
value.setText(item.parsedValue);
|
||||
if(item.verifiedAt!=null){
|
||||
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
|
||||
value.setTextColor(textColor);
|
||||
value.setLinkTextColor(textColor);
|
||||
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
|
||||
check.setTint(textColor);
|
||||
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
|
||||
}else{
|
||||
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
|
||||
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
|
||||
value.setCompoundDrawables(null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index];
|
||||
span.setDrawable(image);
|
||||
title.invalidate();
|
||||
value.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
}
|
||||
|
||||
private class EditableAboutViewHolder extends BaseViewHolder {
|
||||
private EditText title;
|
||||
private EditText value;
|
||||
|
||||
public EditableAboutViewHolder(){
|
||||
super(R.layout.item_profile_about_editable);
|
||||
title=findViewById(R.id.title);
|
||||
value=findViewById(R.id.value);
|
||||
findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
|
||||
dragHelper.startDrag(this);
|
||||
return true;
|
||||
});
|
||||
title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString()));
|
||||
value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString()));
|
||||
findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountField item){
|
||||
title.setText(item.name);
|
||||
value.setText(item.value);
|
||||
}
|
||||
|
||||
private void onRemoveRowClick(View v){
|
||||
int pos=getAbsoluteAdapterPosition();
|
||||
metadataListData.remove(pos);
|
||||
adapter.notifyItemRemoved(pos);
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
BaseViewHolder vh=(BaseViewHolder) list.getChildViewHolder(list.getChildAt(i));
|
||||
vh.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AddRowViewHolder extends BaseViewHolder implements UsableRecyclerView.Clickable{
|
||||
public AddRowViewHolder(){
|
||||
super(R.layout.item_profile_about_add_row);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
metadataListData.add(new AccountField());
|
||||
if(metadataListData.size()==MAX_FIELDS){ // replace this row with new row
|
||||
adapter.notifyItemChanged(metadataListData.size()-1);
|
||||
}else{
|
||||
adapter.notifyItemInserted(metadataListData.size()-1);
|
||||
rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountField item) {}
|
||||
}
|
||||
|
||||
private class ReorderCallback extends ItemTouchHelper.SimpleCallback{
|
||||
public ReorderCallback(){
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){
|
||||
if(target instanceof AddRowViewHolder)
|
||||
return false;
|
||||
int fromPosition=viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition=target.getAbsoluteAdapterPosition();
|
||||
if (fromPosition<toPosition) {
|
||||
for (int i=fromPosition;i<toPosition;i++) {
|
||||
Collections.swap(metadataListData, i, i+1);
|
||||
}
|
||||
} else {
|
||||
for (int i=fromPosition;i>toPosition;i--) {
|
||||
Collections.swap(metadataListData, i, i-1);
|
||||
}
|
||||
}
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
((BindableViewHolder)viewHolder).rebind();
|
||||
((BindableViewHolder)target).rebind();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
|
||||
super.onSelectedChanged(viewHolder, actionState);
|
||||
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
|
||||
viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
|
||||
viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
draggedViewHolder=viewHolder;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
draggedViewHolder=null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled(){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@ import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ScheduledStatusListFragment extends BaseStatusListFragment<ScheduledStatus> {
|
||||
private String nextMaxID;
|
||||
private ImageButton fab;
|
||||
private static final int SCHEDULED_STATUS_LIST_OPENED = 161;
|
||||
|
||||
public ScheduledStatusListFragment() {
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,14 +56,24 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
protected void onFabClick(View v) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
|
||||
fab.setOnClickListener(v -> Nav.go(getActivity(), ComposeFragment.class, args));
|
||||
fab.setOnLongClickListener(v -> UiUtils.pickAccountForCompose(getActivity(), accountID, args));
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onFabLongClick(View v) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("scheduledAt", CreateStatus.getDraftInstant());
|
||||
return UiUtils.pickAccountForCompose(getActivity(), accountID, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if (getArguments().getBoolean("hide_fab", false)) fab.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@@ -109,6 +119,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,6 +9,8 @@ import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.LruCache;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -41,11 +43,13 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
@@ -63,7 +67,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -77,6 +80,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private ThemeItem themeItem;
|
||||
private NotificationPolicyItem notificationPolicyItem;
|
||||
private SwitchItem showNewPostsButtonItem, glitchModeItem;
|
||||
private String accountID;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private boolean needAppRestart;
|
||||
@@ -163,12 +167,6 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
GlobalUserPreferences.reduceMotion=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_alt_indicator, R.drawable.ic_fluent_scan_text_24_regular, GlobalUserPreferences.showAltIndicator, i->{
|
||||
GlobalUserPreferences.showAltIndicator=i.checked;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_no_alt_indicator, R.drawable.ic_fluent_important_24_regular, GlobalUserPreferences.showNoAltIndicator, i->{
|
||||
GlobalUserPreferences.showNoAltIndicator=i.checked;
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_behavior));
|
||||
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
|
||||
@@ -205,6 +203,51 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
GlobalUserPreferences.keepOnlyLatestNotification=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_prefix_reply_cw_with_re, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.prefixRepliesWithRe, i->{
|
||||
GlobalUserPreferences.prefixRepliesWithRe=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_timelines));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
||||
GlobalUserPreferences.showReplies=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
|
||||
GlobalUserPreferences.showBoosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_sync_24_regular, GlobalUserPreferences.loadNewPosts, i->{
|
||||
GlobalUserPreferences.loadNewPosts=i.checked;
|
||||
showNewPostsButtonItem.enabled = i.checked;
|
||||
if (!i.checked) {
|
||||
GlobalUserPreferences.showNewPostsButton = false;
|
||||
showNewPostsButtonItem.checked = false;
|
||||
}
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(showNewPostsButtonItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(showNewPostsButtonItem = new SwitchItem(R.string.sk_settings_see_new_posts_button, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.showNewPostsButton, i->{
|
||||
GlobalUserPreferences.showNewPostsButton=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_alt_indicator, R.drawable.ic_fluent_scan_text_24_regular, GlobalUserPreferences.showAltIndicator, i->{
|
||||
GlobalUserPreferences.showAltIndicator=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_no_alt_indicator, R.drawable.ic_fluent_important_24_regular, GlobalUserPreferences.showNoAltIndicator, i->{
|
||||
GlobalUserPreferences.showNoAltIndicator=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_collapse_long_posts, R.drawable.ic_fluent_chevron_down_24_filled, GlobalUserPreferences.collapseLongPosts, i->{
|
||||
GlobalUserPreferences.collapseLongPosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_spectator_mode, R.drawable.ic_fluent_eye_24_regular, GlobalUserPreferences.spectatorMode, i->{
|
||||
GlobalUserPreferences.spectatorMode=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
needAppRestart=true;
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{
|
||||
GlobalUserPreferences.translateButtonOpenedOnly=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
@@ -215,29 +258,18 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
R.string.sk_settings_translation_availability_note_available :
|
||||
R.string.sk_settings_translation_availability_note_unavailable, instanceName)));
|
||||
|
||||
items.add(new HeaderItem(R.string.home_timeline));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_replies, R.drawable.ic_fluent_chat_multiple_24_regular, GlobalUserPreferences.showReplies, i->{
|
||||
GlobalUserPreferences.showReplies=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_show_boosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, GlobalUserPreferences.showBoosts, i->{
|
||||
GlobalUserPreferences.showBoosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.sk_settings_load_new_posts, R.drawable.ic_fluent_arrow_up_24_regular, GlobalUserPreferences.loadNewPosts, i->{
|
||||
GlobalUserPreferences.loadNewPosts=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_notifications));
|
||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||
PushSubscription pushSubscription=getPushSubscription();
|
||||
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
|
||||
items.add(new SwitchItem(R.string.sk_notify_update, R.drawable.ic_fluent_history_24_regular, pushSubscription.alerts.update, i->onNotificationsChanged(PushNotification.Type.UPDATE, i.checked)));
|
||||
items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_alert_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked)));
|
||||
boolean switchEnabled=pushSubscription.policy!=PushSubscription.Policy.NONE;
|
||||
|
||||
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked), switchEnabled));
|
||||
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked), switchEnabled));
|
||||
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked), switchEnabled));
|
||||
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_fluent_mention_24_regular, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked), switchEnabled));
|
||||
items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_chat_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked), switchEnabled));
|
||||
items.add(new SwitchItem(R.string.sk_notify_update, R.drawable.ic_fluent_history_24_regular, pushSubscription.alerts.update, i->onNotificationsChanged(PushNotification.Type.UPDATE, i.checked), switchEnabled));
|
||||
items.add(new SwitchItem(R.string.sk_notify_poll_results, R.drawable.ic_fluent_poll_24_regular, pushSubscription.alerts.poll, i->onNotificationsChanged(PushNotification.Type.POLL, i.checked), switchEnabled));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_account));
|
||||
items.add(new TextItem(R.string.sk_settings_profile, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/profile"), R.drawable.ic_fluent_open_24_regular));
|
||||
@@ -255,11 +287,39 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut, R.drawable.ic_fluent_sign_out_24_regular));
|
||||
if (!TextUtils.isEmpty(instance.version)) items.add(new SmallTextItem(getString(R.string.sk_settings_server_version, instance.version)));
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_instance_features));
|
||||
items.add(new SwitchItem(R.string.sk_settings_support_local_only, 0, GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID), i->{
|
||||
glitchModeItem.enabled = i.checked;
|
||||
if (i.checked) {
|
||||
GlobalUserPreferences.accountsWithLocalOnlySupport.add(accountID);
|
||||
if (instance.pleroma == null) GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
||||
} else {
|
||||
GlobalUserPreferences.accountsWithLocalOnlySupport.remove(accountID);
|
||||
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
|
||||
}
|
||||
glitchModeItem.checked = GlobalUserPreferences.accountsInGlitchMode.contains(accountID);
|
||||
if (list.findViewHolderForAdapterPosition(items.indexOf(glitchModeItem)) instanceof SwitchViewHolder svh) svh.rebind();
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SmallTextItem(getString(R.string.sk_settings_local_only_explanation)));
|
||||
items.add(glitchModeItem = new SwitchItem(R.string.sk_settings_glitch_instance, 0, GlobalUserPreferences.accountsInGlitchMode.contains(accountID), i->{
|
||||
if (i.checked) {
|
||||
GlobalUserPreferences.accountsInGlitchMode.add(accountID);
|
||||
} else {
|
||||
GlobalUserPreferences.accountsInGlitchMode.remove(accountID);
|
||||
}
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
glitchModeItem.enabled = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
|
||||
items.add(new SmallTextItem(getString(R.string.sk_settings_glitch_mode_explanation)));
|
||||
|
||||
items.add(new HeaderItem(R.string.sk_settings_about));
|
||||
items.add(new TextItem(R.string.sk_settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"), R.drawable.ic_fluent_open_24_regular));
|
||||
items.add(new TextItem(R.string.sk_settings_donate, ()->UiUtils.launchWebBrowser(getActivity(), "https://ko-fi.com/xsk22"), R.drawable.ic_fluent_heart_24_regular));
|
||||
clearImageCacheItem = new TextItem(R.string.settings_clear_cache, UiUtils.formatFileSize(getContext(), imageCache.getDiskCache().size(), true), this::clearImageCache, 0);
|
||||
LruCache<?, ?> cache = imageCache == null ? null : imageCache.getLruCache();
|
||||
clearImageCacheItem = new TextItem(R.string.settings_clear_cache, UiUtils.formatFileSize(getContext(), cache != null ? cache.size() : 0, true), this::clearImageCache, 0);
|
||||
items.add(clearImageCacheItem);
|
||||
items.add(new TextItem(R.string.sk_clear_recent_languages, ()->UiUtils.showConfirmationAlert(getActivity(), R.string.sk_clear_recent_languages, R.string.sk_confirm_clear_recent_languages, R.string.clear, ()->{
|
||||
GlobalUserPreferences.recentLanguages.remove(accountID);
|
||||
@@ -274,6 +334,19 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
items.add(checkForUpdateItem);
|
||||
}
|
||||
|
||||
if(BuildConfig.DEBUG){
|
||||
items.add(new RedHeaderItem("Debug options"));
|
||||
items.add(new TextItem("Test e-mail confirmation flow", ()->{
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}));
|
||||
}
|
||||
|
||||
items.add(new FooterItem(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
||||
}
|
||||
|
||||
@@ -429,7 +502,8 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
case FAVORITE -> subscription.alerts.favourite=enabled;
|
||||
case FOLLOW -> subscription.alerts.follow=enabled;
|
||||
case REBLOG -> subscription.alerts.reblog=enabled;
|
||||
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
|
||||
case MENTION -> subscription.alerts.mention=enabled;
|
||||
case POLL -> subscription.alerts.poll=enabled;
|
||||
case STATUS -> subscription.alerts.status=enabled;
|
||||
case UPDATE -> subscription.alerts.update=enabled;
|
||||
}
|
||||
@@ -450,9 +524,13 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
list.getAdapter().notifyItemChanged(index);
|
||||
}
|
||||
if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
|
||||
boolean newState=policy!=PushSubscription.Policy.NONE;
|
||||
for(PushNotification.Type value : PushNotification.Type.values()){
|
||||
onNotificationsChanged(value, newState);
|
||||
}
|
||||
index++;
|
||||
while(items.get(index) instanceof SwitchItem si){
|
||||
si.enabled=si.checked=policy!=PushSubscription.Policy.NONE;
|
||||
si.enabled=si.checked=newState;
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
|
||||
if(holder!=null)
|
||||
((BindableViewHolder<?>)holder).rebind();
|
||||
@@ -492,6 +570,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
}
|
||||
|
||||
private void onLoggedOut(){
|
||||
if (getActivity() == null) return;
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
@@ -569,7 +648,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
this.onChanged=onChanged;
|
||||
}
|
||||
|
||||
public SwitchItem(@StringRes int text, int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
|
||||
public SwitchItem(@StringRes int text, @DrawableRes int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
|
||||
this.text=getString(text);
|
||||
this.icon=icon;
|
||||
this.checked=checked;
|
||||
@@ -656,6 +735,11 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
this.secondaryText = secondaryText;
|
||||
}
|
||||
|
||||
public TextItem(String text, Runnable onClick){
|
||||
this.text=text;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 4;
|
||||
@@ -668,6 +752,10 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
super(text);
|
||||
}
|
||||
|
||||
public RedHeaderItem(String text){
|
||||
super(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 5;
|
||||
@@ -940,19 +1028,19 @@ public class SettingsFragment extends MastodonToolbarFragment{
|
||||
|
||||
private class SmallTextViewHolder extends BindableViewHolder<SmallTextItem> {
|
||||
private final TextView text;
|
||||
;
|
||||
|
||||
public SmallTextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text = itemView.findViewById(R.id.text);
|
||||
text.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
|
||||
text.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
text.setPaddingRelative(text.getPaddingStart(), 0, text.getPaddingEnd(), text.getPaddingBottom());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SmallTextItem item){
|
||||
text.setText(item.text);
|
||||
text.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
|
||||
text.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,16 +173,7 @@ public class SplashFragment extends AppKitFragment{
|
||||
TextView title=new TextView(getActivity());
|
||||
title.setTextAppearance(R.style.m3_headline_medium);
|
||||
title.setText(switch(page){
|
||||
case 0 -> {
|
||||
String src=getString(R.string.welcome_page1_title);
|
||||
SpannableString ss=new SpannableString(src);
|
||||
int start=src.indexOf("{logo}");
|
||||
if(start!=-1){
|
||||
LogoSpan span=new LogoSpan(getResources().getDrawable(R.drawable.splash_logo, getActivity().getTheme()));
|
||||
ss.setSpan(span, start, start+6, 0);
|
||||
}
|
||||
yield ss;
|
||||
}
|
||||
case 0 -> getString(R.string.welcome_page1_title);
|
||||
case 1 -> getString(R.string.welcome_page2_title);
|
||||
case 2 -> getString(R.string.welcome_page3_title);
|
||||
default -> throw new IllegalStateException("Unexpected value: "+page);
|
||||
@@ -204,26 +195,4 @@ public class SplashFragment extends AppKitFragment{
|
||||
ll.addView(text, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
}
|
||||
|
||||
private class LogoSpan extends ReplacementSpan{
|
||||
private final Drawable drawable;
|
||||
|
||||
private LogoSpan(Drawable drawable){
|
||||
this.drawable=drawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
|
||||
return drawable.getIntrinsicWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
|
||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||
canvas.save();
|
||||
canvas.translate(x, y-V.dp(20));
|
||||
drawable.draw(canvas);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
Collections.sort(result, Comparator.comparing((Status s)->s.createdAt).reversed());
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
})
|
||||
@@ -139,7 +140,8 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
action=getString(R.string.edit_multiple_changed);
|
||||
}
|
||||
}
|
||||
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" · "+date, Collections.emptyList(), 0, null, null));
|
||||
String sep = getString(R.string.sk_separator);
|
||||
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import android.os.Bundle;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
@@ -30,7 +32,9 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
protected EventListener eventListener=new EventListener();
|
||||
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true, null);
|
||||
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
|
||||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, Filter.FilterContext.HOME);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,6 +60,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
|
||||
Status status=getContentStatusByID(id);
|
||||
if(status==null)
|
||||
return;
|
||||
status.filterRevealed = true;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(status));
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.util.stream.Collectors;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ThreadFragment extends StatusListFragment{
|
||||
private Status mainStatus;
|
||||
protected Status mainStatus;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -61,8 +61,7 @@ public class ThreadFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(StatusContext result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if (getActivity() == null) return;
|
||||
if(refreshing){
|
||||
data.clear();
|
||||
displayItems.clear();
|
||||
|
||||
@@ -101,6 +101,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
for(Relationship rel:result){
|
||||
relationships.put(rel.id, rel);
|
||||
}
|
||||
if (getActivity() == null) return;
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
@@ -128,7 +129,8 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// list.setPadding(0, V.dp(16), 0, V.dp(16));
|
||||
list.setClipToPadding(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 72, 16));
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1,
|
||||
Math.round(16f + 56f * getResources().getConfiguration().fontScale), 16));
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
@@ -370,6 +372,7 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment<BaseA
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
relationships.put(AccountViewHolder.this.item.account.id, result);
|
||||
if (getActivity() == null) return;
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(result.stream().map(AccountItem::new).collect(Collectors.toList()), nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -74,6 +74,7 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(result.stream().map(fs->new AccountWrapper(fs.account)).collect(Collectors.toList()), false);
|
||||
loadRelationships();
|
||||
}
|
||||
@@ -108,6 +109,7 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
|
||||
public void onSuccess(List<Relationship> result){
|
||||
relationshipsRequest=null;
|
||||
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
|
||||
if (getActivity() == null) return;
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
|
||||
@@ -59,6 +59,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
|
||||
imageRequests=result.stream()
|
||||
.map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150)))
|
||||
.collect(Collectors.toList());
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,10 +6,13 @@ import android.view.View;
|
||||
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
|
||||
import org.joinmastodon.android.fragments.IsOnTop;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
@@ -22,6 +25,8 @@ public class DiscoverPostsFragment extends StatusListFragment implements IsOnTop
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.FabStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -17,10 +16,16 @@ import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FederatedTimelineFragment extends FabStatusListFragment {
|
||||
public class FederatedTimelineFragment extends StatusListFragment {
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
|
||||
private String maxID;
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
|
||||
@@ -29,7 +34,9 @@ public class FederatedTimelineFragment extends FabStatusListFragment {
|
||||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -3,9 +3,7 @@ package org.joinmastodon.android.fragments.discover;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.FabStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
@@ -17,10 +15,16 @@ import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class LocalTimelineFragment extends FabStatusListFragment {
|
||||
public class LocalTimelineFragment extends StatusListFragment {
|
||||
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
|
||||
private String maxID;
|
||||
|
||||
@Override
|
||||
protected boolean withComposeButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
|
||||
@@ -29,7 +33,9 @@ public class LocalTimelineFragment extends FabStatusListFragment {
|
||||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
if (getActivity() == null) return;
|
||||
result=result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList());
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -124,6 +124,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if (getActivity() == null) return;
|
||||
resetEmptyText();
|
||||
if(isInRecentMode()){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{
|
||||
@@ -156,6 +157,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> impleme
|
||||
}
|
||||
prevDisplayItems=new ArrayList<>(displayItems);
|
||||
unfilteredResults=results;
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(filterSearchResults(results), false);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Hashtag> result){
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -193,30 +193,24 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
mgr.removeAccount(accountID);
|
||||
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, null);
|
||||
String newID=mgr.getLastActiveAccountID();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", newID);
|
||||
if(session.self.avatar!=null || session.self.displayName!=null){
|
||||
File avaFile=session.self.avatar!=null ? new File(session.self.avatar) : null;
|
||||
new UpdateAccountCredentials(session.self.displayName, "", avaFile, null, Collections.emptyList())
|
||||
accountID=newID;
|
||||
if((session.self.avatar!=null || session.self.displayName!=null) && !getArguments().getBoolean("debug")){
|
||||
new UpdateAccountCredentials(session.self.displayName, "", (File)null, null, Collections.emptyList())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
if(avaFile!=null)
|
||||
avaFile.delete();
|
||||
mgr.updateAccountInfo(newID, result);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
proceed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(avaFile!=null)
|
||||
avaFile.delete();
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
proceed();
|
||||
}
|
||||
})
|
||||
.exec(newID);
|
||||
}else{
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
proceed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,4 +243,11 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
super.onDestroyView();
|
||||
resendBtn.removeCallbacks(resendTimer);
|
||||
}
|
||||
|
||||
private void proceed(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
// Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
Nav.goClearingStack(getActivity(), OnboardingProfileSetupFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -19,6 +20,7 @@ import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.parceler.Parcels;
|
||||
@@ -42,6 +44,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
@@ -58,6 +61,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private Call currentRequest;
|
||||
private ItemsAdapter itemsAdapter;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
|
||||
private static final int SIGNUP_REQUEST=722;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -72,7 +78,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
||||
|
||||
items.add(new Item("Mastodon for Android Privacy Policy", "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
|
||||
items.add(new Item("Mastodon for Android Privacy Policy", getString(R.string.privacy_policy_explanation), "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
|
||||
loadServerPrivacyPolicy();
|
||||
}
|
||||
|
||||
@@ -93,18 +99,24 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
|
||||
TextView text=headerView.findViewById(R.id.text);
|
||||
text.setText(R.string.privacy_policy_subtitle);
|
||||
text.setText(getString(R.string.privacy_policy_subtitle, instance.uri));
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(itemsAdapter=new ItemsAdapter());
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
Button backBtn=view.findViewById(R.id.btn_back);
|
||||
backBtn.setText(getString(R.string.server_policy_disagree, instance.uri));
|
||||
backBtn.setOnClickListener(v->{
|
||||
setResult(false, null);
|
||||
Nav.finish(this);
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -113,19 +125,32 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackground(null);
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), SignupFragment.class, args);
|
||||
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
super.onFragmentResult(reqCode, success, result);
|
||||
if(reqCode==SIGNUP_REQUEST && !success){
|
||||
setResult(false, null);
|
||||
Nav.finish(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -158,7 +183,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
if(!response.isSuccessful())
|
||||
return;
|
||||
Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString());
|
||||
final Item item=new Item(doc.title(), instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
|
||||
final Item item=new Item(doc.title(), null, instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
|
||||
Activity activity=getActivity();
|
||||
if(activity!=null){
|
||||
activity.runOnUiThread(()->{
|
||||
@@ -192,16 +217,23 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
|
||||
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final TextView subtitle;
|
||||
|
||||
public ItemViewHolder(){
|
||||
super(getActivity(), R.layout.item_privacy_policy_link, list);
|
||||
title=findViewById(R.id.title);
|
||||
title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
||||
subtitle=findViewById(R.id.subtitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Item item){
|
||||
title.setText(item.title);
|
||||
if(TextUtils.isEmpty(item.subtitle)){
|
||||
subtitle.setVisibility(View.GONE);
|
||||
}else{
|
||||
subtitle.setVisibility(View.VISIBLE);
|
||||
subtitle.setText(item.subtitle);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -211,10 +243,11 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
}
|
||||
|
||||
private static class Item{
|
||||
public String title, domain, url, faviconUrl;
|
||||
public String title, subtitle, domain, url, faviconUrl;
|
||||
|
||||
public Item(String title, String domain, String url, String faviconUrl){
|
||||
public Item(String title, String subtitle, String domain, String url, String faviconUrl){
|
||||
this.title=title;
|
||||
this.subtitle=subtitle;
|
||||
this.domain=domain;
|
||||
this.url=url;
|
||||
this.faviconUrl=faviconUrl;
|
||||
|
||||
@@ -92,7 +92,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
|
||||
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
||||
return true;
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
||||
updateFilteredList();
|
||||
searchEdit.removeCallbacks(searchDebouncer);
|
||||
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
|
||||
@@ -106,7 +106,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
|
||||
}
|
||||
|
||||
protected void onSearchChangedDebounced(){
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase();
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
||||
updateFilteredList();
|
||||
loadInstanceInfo(currentSearchQuery, false);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
@@ -20,7 +14,6 @@ import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RadioButton;
|
||||
@@ -38,6 +31,7 @@ import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FilterChipView;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -56,11 +50,7 @@ import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
@@ -215,47 +205,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
setStatusBarColor(0);
|
||||
topBar=view.findViewById(R.id.top_bar);
|
||||
|
||||
LayerDrawable topBg=(LayerDrawable) topBar.getBackground().mutate();
|
||||
topBar.setBackground(topBg);
|
||||
Drawable topOverlay=topBg.findDrawableByLayerId(R.id.color_overlay);
|
||||
topOverlay.setAlpha(0);
|
||||
|
||||
LayerDrawable btmBg=(LayerDrawable) buttonBar.getBackground().mutate();
|
||||
buttonBar.setBackground(btmBg);
|
||||
Drawable btmOverlay=btmBg.findDrawableByLayerId(R.id.color_overlay);
|
||||
btmOverlay.setAlpha(0);
|
||||
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
private boolean isAtTop=true;
|
||||
private Animator currentPanelsAnim;
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
|
||||
if(newAtTop!=isAtTop){
|
||||
isAtTop=newAtTop;
|
||||
if(currentPanelsAnim!=null)
|
||||
currentPanelsAnim.cancel();
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(topOverlay, "alpha", isAtTop ? 0 : 20),
|
||||
ObjectAnimator.ofInt(btmOverlay, "alpha", isAtTop ? 0 : 20),
|
||||
ObjectAnimator.ofFloat(topBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3)),
|
||||
ObjectAnimator.ofFloat(buttonBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3))
|
||||
);
|
||||
set.setDuration(150);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
currentPanelsAnim=null;
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
currentPanelsAnim=set;
|
||||
}
|
||||
}
|
||||
});
|
||||
list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar));
|
||||
|
||||
searchEdit=view.findViewById(R.id.search_edit);
|
||||
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
|
||||
@@ -366,6 +316,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
}).collect(Collectors.toList());
|
||||
focusThing=view.findViewById(R.id.focus_thing);
|
||||
focusThing.requestFocus();
|
||||
|
||||
view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
|
||||
nextButton.setEnabled(chosenInstance!=null);
|
||||
}
|
||||
|
||||
private void onRegionFilterClick(View v){
|
||||
@@ -396,22 +349,6 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
updateFilteredList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNextClick(View v){
|
||||
if(chosenInstance==null){
|
||||
String lang=Locale.getDefault().getLanguage();
|
||||
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
|
||||
if(instances.isEmpty()){
|
||||
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
|
||||
}
|
||||
if(instances.isEmpty()){
|
||||
return;
|
||||
}
|
||||
chosenInstance=instances.get(new Random().nextInt(instances.size()));
|
||||
}
|
||||
super.onNextClick(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void proceedWithAuthOrSignup(Instance instance){
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
@@ -428,6 +365,19 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}
|
||||
|
||||
private void onPickRandomInstanceClick(View v){
|
||||
String lang=Locale.getDefault().getLanguage();
|
||||
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
|
||||
if(instances.isEmpty()){
|
||||
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
|
||||
}
|
||||
if(instances.isEmpty()){
|
||||
return;
|
||||
}
|
||||
chosenInstance=instances.get(new Random().nextInt(instances.size()));
|
||||
onNextClick(v);
|
||||
}
|
||||
|
||||
// private String getEmojiForCategory(String category){
|
||||
// return switch(category){
|
||||
// case "all" -> "💬";
|
||||
@@ -577,7 +527,16 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
updateFilteredList();
|
||||
}
|
||||
|
||||
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!searchQueryMode){
|
||||
// Prevent search view automatically getting focused when the user returns to this fragment
|
||||
focusThing.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder>{
|
||||
public InstancesAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
@@ -603,22 +562,11 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
public int getItemViewType(int position){
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return filteredData.get(position).thumbnailRequest!=null ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
return filteredData.get(position).thumbnailRequest;
|
||||
}
|
||||
}
|
||||
|
||||
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.DisableableClickable, ImageLoaderViewHolder{
|
||||
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.DisableableClickable{
|
||||
private final TextView title, description;
|
||||
private final RadioButton radioButton;
|
||||
private final ImageView thumbnail;
|
||||
private boolean enabled;
|
||||
|
||||
public InstanceViewHolder(){
|
||||
@@ -626,15 +574,12 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
title=findViewById(R.id.title);
|
||||
description=findViewById(R.id.description);
|
||||
radioButton=findViewById(R.id.radiobtn);
|
||||
thumbnail=findViewById(R.id.image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(CatalogInstance item){
|
||||
title.setText(item.normalizedDomain);
|
||||
radioButton.setChecked(chosenInstance==item);
|
||||
if(item.thumbnailRequest==null)
|
||||
thumbnail.setImageDrawable(null);
|
||||
Instance realInstance=instancesCache.get(item.normalizedDomain);
|
||||
float alpha;
|
||||
if(realInstance!=null && !realInstance.registrations){
|
||||
@@ -649,7 +594,6 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
title.setAlpha(alpha);
|
||||
description.setAlpha(alpha);
|
||||
radioButton.setAlpha(alpha);
|
||||
thumbnail.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -672,6 +616,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
adapter.notifyItemChanged(idx);
|
||||
}
|
||||
}
|
||||
if(!nextButton.isEnabled()){
|
||||
nextButton.setEnabled(true);
|
||||
}
|
||||
radioButton.setChecked(true);
|
||||
if(chosenInstance==null)
|
||||
nextButton.setEnabled(true);
|
||||
@@ -679,16 +626,6 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
loadInstanceInfo(chosenInstance.domain, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
thumbnail.setImageDrawable(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return enabled;
|
||||
@@ -710,4 +647,5 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
return (this==GENERAL)==isGeneral;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -28,6 +29,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class InstanceRulesFragment extends ToolbarFragment{
|
||||
@@ -36,6 +38,9 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private Instance instance;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
|
||||
private static final int RULES_REQUEST=376;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -71,6 +76,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -79,19 +86,31 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
// view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackground(null);
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
Nav.go(getActivity(), GoogleMadeMeAddThisFragment.class, args);
|
||||
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
super.onFragmentResult(reqCode, success, result);
|
||||
if(reqCode==RULES_REQUEST && !success){
|
||||
Nav.finish(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.model.FollowSuggestion;
|
||||
import org.joinmastodon.android.model.ParsedAccount;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<ParsedAccount>{
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
private View buttonBar;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
private int numRunningFollowRequests=0;
|
||||
|
||||
public OnboardingFollowSuggestionsFragment(){
|
||||
super(R.layout.fragment_onboarding_follow_suggestions, 40);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
setTitle(R.string.popular_on_mastodon);
|
||||
accountID=getArguments().getString("account");
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
|
||||
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
|
||||
view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetFollowSuggestions(40)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
loadRelationships();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void loadRelationships(){
|
||||
relationships=Collections.emptyMap();
|
||||
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
|
||||
relationshipsRequest.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Relationship> result){
|
||||
relationshipsRequest=null;
|
||||
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
|
||||
if(list==null)
|
||||
return;
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
||||
if(holder instanceof SuggestionViewHolder svh)
|
||||
svh.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
relationshipsRequest=null;
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return new SuggestionsAdapter();
|
||||
}
|
||||
|
||||
private void onFollowAllClick(View v){
|
||||
if(!loaded || relationships.isEmpty())
|
||||
return;
|
||||
if(data.isEmpty()){
|
||||
proceed();
|
||||
return;
|
||||
}
|
||||
ArrayList<String> accountIdsToFollow=new ArrayList<>();
|
||||
for(ParsedAccount acc:data){
|
||||
Relationship rel=relationships.get(acc.account.id);
|
||||
if(rel==null)
|
||||
continue;
|
||||
if(rel.canFollow())
|
||||
accountIdsToFollow.add(acc.account.id);
|
||||
}
|
||||
|
||||
final ProgressDialog progress=new ProgressDialog(getActivity());
|
||||
progress.setIndeterminate(false);
|
||||
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
progress.setMax(accountIdsToFollow.size());
|
||||
progress.setCancelable(false);
|
||||
progress.setMessage(getString(R.string.sending_follows));
|
||||
progress.show();
|
||||
|
||||
for(int i=0;i<Math.min(accountIdsToFollow.size(), 5);i++){ // Send up to 5 requests in parallel
|
||||
followNextAccount(accountIdsToFollow, progress);
|
||||
}
|
||||
}
|
||||
|
||||
private void followNextAccount(ArrayList<String> accountIdsToFollow, ProgressDialog progress){
|
||||
if(accountIdsToFollow.isEmpty()){
|
||||
if(numRunningFollowRequests==0){
|
||||
progress.dismiss();
|
||||
proceed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
numRunningFollowRequests++;
|
||||
String id=accountIdsToFollow.remove(0);
|
||||
new SetAccountFollowed(id, true, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
numRunningFollowRequests--;
|
||||
progress.setProgress(progress.getMax()-accountIdsToFollow.size()-numRunningFollowRequests);
|
||||
followNextAccount(accountIdsToFollow, progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
numRunningFollowRequests--;
|
||||
progress.setProgress(progress.getMax()-accountIdsToFollow.size()-numRunningFollowRequests);
|
||||
followNextAccount(accountIdsToFollow, progress);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void proceed(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), HomeFragment.class, args);
|
||||
getActivity().getWindow().getDecorView().postDelayed(()->Nav.finish(this), 500);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canGoBack(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarNavigationClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
}
|
||||
|
||||
private class SuggestionsAdapter extends UsableRecyclerView.Adapter<SuggestionViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public SuggestionsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new SuggestionViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SuggestionViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return data.get(position).emojiHelper.getImageCount()+1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
ParsedAccount account=data.get(position);
|
||||
if(image==0)
|
||||
return account.avatarRequest;
|
||||
return account.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
}
|
||||
|
||||
private class SuggestionViewHolder extends BindableViewHolder<ParsedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final TextView name, username, bio;
|
||||
private final ImageView avatar;
|
||||
private final ProgressBarButton actionButton;
|
||||
private final ProgressBar actionProgress;
|
||||
private final View actionWrap;
|
||||
|
||||
private Relationship relationship;
|
||||
|
||||
public SuggestionViewHolder(){
|
||||
super(getActivity(), R.layout.item_user_row_m3, list);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
bio=findViewById(R.id.bio);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
actionButton=findViewById(R.id.action_btn);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
actionWrap=findViewById(R.id.action_btn_wrap);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
actionButton.setOnClickListener(UiUtils.rateLimitedClickListener(this::onActionButtonClick));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ParsedAccount item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText(item.account.getDisplayUsername());
|
||||
if(TextUtils.isEmpty(item.parsedBio)){
|
||||
bio.setVisibility(View.GONE);
|
||||
}else{
|
||||
bio.setVisibility(View.VISIBLE);
|
||||
bio.setText(item.parsedBio);
|
||||
}
|
||||
|
||||
relationship=relationships.get(item.account.id);
|
||||
if(relationship==null){
|
||||
actionWrap.setVisibility(View.GONE);
|
||||
}else{
|
||||
actionWrap.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
bio.invalidate();
|
||||
}
|
||||
if(image instanceof Animatable a && !a.isRunning())
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
private void onActionButtonClick(View v){
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
rebind();
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
actionButton.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
actionButton.setClickable(!visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class OnboardingProfileSetupFragment extends ToolbarFragment implements ReorderableLinearLayout.OnDragListener{
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private String accountID;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
private ScrollView scroller;
|
||||
private EditText nameEdit, bioEdit;
|
||||
private ImageView avaImage, coverImage;
|
||||
private Button addRow;
|
||||
private ReorderableLinearLayout profileFieldsLayout;
|
||||
private Uri avatarUri, coverUri;
|
||||
|
||||
private static final int AVATAR_RESULT=348;
|
||||
private static final int COVER_RESULT=183;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(R.string.profile_setup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=inflater.inflate(R.layout.fragment_onboarding_profile_setup, container, false);
|
||||
|
||||
scroller=view.findViewById(R.id.scroller);
|
||||
nameEdit=view.findViewById(R.id.display_name);
|
||||
bioEdit=view.findViewById(R.id.bio);
|
||||
avaImage=view.findViewById(R.id.avatar);
|
||||
coverImage=view.findViewById(R.id.header);
|
||||
addRow=view.findViewById(R.id.add_row);
|
||||
profileFieldsLayout=view.findViewById(R.id.profile_fields);
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
avaImage.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
avaImage.setClipToOutline(true);
|
||||
|
||||
Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
if(savedInstanceState==null){
|
||||
nameEdit.setText(account.displayName);
|
||||
makeFieldsRow();
|
||||
}else{
|
||||
ArrayList<String> fieldTitles=savedInstanceState.getStringArrayList("fieldTitles");
|
||||
ArrayList<String> fieldValues=savedInstanceState.getStringArrayList("fieldValues");
|
||||
for(int i=0;i<fieldTitles.size();i++){
|
||||
View row=makeFieldsRow();
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
title.setText(fieldTitles.get(i));
|
||||
content.setText(fieldValues.get(i));
|
||||
}
|
||||
if(fieldTitles.size()==4)
|
||||
addRow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
addRow.setOnClickListener(v->{
|
||||
makeFieldsRow();
|
||||
if(profileFieldsLayout.getChildCount()==4){
|
||||
addRow.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
profileFieldsLayout.setDragListener(this);
|
||||
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
|
||||
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
scroller.setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
protected void onButtonClick(){
|
||||
ArrayList<AccountField> fields=new ArrayList<>();
|
||||
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
|
||||
View row=profileFieldsLayout.getChildAt(i);
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
AccountField fld=new AccountField();
|
||||
fld.name=title.getText().toString();
|
||||
fld.value=content.getText().toString();
|
||||
fields.add(fld);
|
||||
}
|
||||
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, fields)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
AccountSessionManager.getInstance().updateAccountInfo(accountID, result);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
||||
getActivity().getWindow().getDecorView().postDelayed(()->Nav.finish(OnboardingProfileSetupFragment.this), 500);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.saving, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
|
||||
}else{
|
||||
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
|
||||
}
|
||||
}
|
||||
|
||||
private View makeFieldsRow(){
|
||||
View view=LayoutInflater.from(getActivity()).inflate(R.layout.onboarding_profile_field, profileFieldsLayout, false);
|
||||
profileFieldsLayout.addView(view);
|
||||
view.findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
|
||||
profileFieldsLayout.startDragging(view);
|
||||
return true;
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwapItems(int oldIndex, int newIndex){}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
ArrayList<String> fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>();
|
||||
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
|
||||
View row=profileFieldsLayout.getChildAt(i);
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
fieldTitles.add(title.getText().toString());
|
||||
fieldValues.add(content.getText().toString());
|
||||
}
|
||||
outState.putStringArrayList("fieldTitles", fieldTitles);
|
||||
outState.putStringArrayList("fieldValues", fieldValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(resultCode!=Activity.RESULT_OK)
|
||||
return;
|
||||
ImageView img;
|
||||
Uri uri=data.getData();
|
||||
int size;
|
||||
if(requestCode==AVATAR_RESULT){
|
||||
img=avaImage;
|
||||
avatarUri=uri;
|
||||
size=V.dp(100);
|
||||
}else{
|
||||
img=coverImage;
|
||||
coverUri=uri;
|
||||
size=V.dp(1000);
|
||||
}
|
||||
img.setForeground(null);
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canGoBack(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onToolbarNavigationClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -16,11 +20,9 @@ import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.MastodonDetailedErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.RegisterAccount;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
@@ -31,18 +33,22 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.ui.text.LinkSpan;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -51,12 +57,10 @@ import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class SignupFragment extends ToolbarFragment{
|
||||
private static final int AVATAR_RESULT=198;
|
||||
private static final String TAG="SignupFragment";
|
||||
|
||||
private Instance instance;
|
||||
@@ -73,6 +77,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
private boolean submitAfterGettingToken;
|
||||
private ProgressDialog progressDialog;
|
||||
private HashSet<EditText> errorFields=new HashSet<>();
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -145,19 +150,22 @@ public class SignupFragment extends ToolbarFragment{
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
|
||||
view.findViewById(R.id.scroller).setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setBackground(null);
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
|
||||
getToolbar().setElevation(0);
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
}
|
||||
|
||||
private void onButtonClick(){
|
||||
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
|
||||
passwordConfirm.setError(getString(R.string.signup_passwords_dont_match));
|
||||
passwordConfirmWrap.setErrorState();
|
||||
passwordConfirmWrap.setErrorState(getString(R.string.signup_passwords_dont_match));
|
||||
return;
|
||||
}
|
||||
showProgressDialog();
|
||||
@@ -212,8 +220,22 @@ public class SignupFragment extends ToolbarFragment{
|
||||
anyFieldsSkipped=true;
|
||||
continue;
|
||||
}
|
||||
field.setError(fieldErrors.get(fieldName).stream().map(err->err.description).collect(Collectors.joining("\n")));
|
||||
getFieldWrapByName(fieldName).setErrorState();
|
||||
List<MastodonDetailedErrorResponse.FieldError> errors=Objects.requireNonNull(fieldErrors.get(fieldName));
|
||||
if(errors.size()==1){
|
||||
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
|
||||
}else{
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
boolean firstErr=true;
|
||||
for(MastodonDetailedErrorResponse.FieldError err:errors){
|
||||
if(firstErr){
|
||||
firstErr=false;
|
||||
}else{
|
||||
ssb.append('\n');
|
||||
}
|
||||
ssb.append(getErrorDescription(err, fieldName));
|
||||
}
|
||||
getFieldWrapByName(fieldName).setErrorState(getErrorDescription(errors.get(0), fieldName));
|
||||
}
|
||||
errorFields.add(field);
|
||||
if(first){
|
||||
first=false;
|
||||
@@ -231,6 +253,40 @@ public class SignupFragment extends ToolbarFragment{
|
||||
.exec(instance.uri, apiToken);
|
||||
}
|
||||
|
||||
private CharSequence getErrorDescription(MastodonDetailedErrorResponse.FieldError error, String fieldName){
|
||||
return switch(fieldName){
|
||||
case "email" -> switch(error.error){
|
||||
case "ERR_BLOCKED" -> {
|
||||
String emailAddr=email.getText().toString();
|
||||
String s=getResources().getString(R.string.signup_email_domain_blocked, TextUtils.htmlEncode(instance.uri), TextUtils.htmlEncode(emailAddr.substring(emailAddr.lastIndexOf('@')+1)));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
Jsoup.parseBodyFragment(s).body().traverse(new NodeVisitor(){
|
||||
private int spanStart;
|
||||
@Override
|
||||
public void head(Node node, int depth){
|
||||
if(node instanceof TextNode tn){
|
||||
ssb.append(tn.text());
|
||||
}else if(node instanceof Element){
|
||||
spanStart=ssb.length();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tail(Node node, int depth){
|
||||
if(node instanceof Element){
|
||||
ssb.setSpan(new LinkSpan("", SignupFragment.this::onGoBackLinkClick, LinkSpan.Type.CUSTOM, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
});
|
||||
yield ssb;
|
||||
}
|
||||
default -> error.description;
|
||||
};
|
||||
default -> error.description;
|
||||
};
|
||||
}
|
||||
|
||||
private EditText getFieldByName(String name){
|
||||
return switch(name){
|
||||
case "email" -> email;
|
||||
@@ -323,6 +379,11 @@ public class SignupFragment extends ToolbarFragment{
|
||||
}
|
||||
}
|
||||
|
||||
private void onGoBackLinkClick(LinkSpan span){
|
||||
setResult(false, null);
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
private class ErrorClearingListener implements TextWatcher{
|
||||
public final EditText editText;
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if (getActivity() == null) return;
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
|
||||
@@ -133,6 +133,14 @@ public class Account extends BaseModel{
|
||||
*/
|
||||
public Instant muteExpiresAt;
|
||||
|
||||
public List<Role> roles;
|
||||
|
||||
@Parcel
|
||||
public static class Role {
|
||||
public String name;
|
||||
/** #rrggbb */
|
||||
public String color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
|
||||
@@ -42,17 +42,9 @@ public class Announcement extends BaseModel implements DisplayItemsParent {
|
||||
}
|
||||
|
||||
public Status toStatus() {
|
||||
Status s = new Status();
|
||||
s.id = id;
|
||||
s.mediaAttachments = List.of();
|
||||
Status s = Status.ofFake(id, content, publishedAt);
|
||||
s.createdAt = startsAt != null ? startsAt : publishedAt;
|
||||
if (updatedAt != null) s.editedAt = updatedAt;
|
||||
s.content = s.text = content;
|
||||
s.spoilerText = "";
|
||||
s.visibility = StatusPrivacy.PUBLIC;
|
||||
s.mentions = List.of();
|
||||
s.tags = List.of();
|
||||
s.emojis = List.of();
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ public class Filter extends BaseModel{
|
||||
public String id;
|
||||
@RequiredField
|
||||
public String phrase;
|
||||
public String title;
|
||||
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);
|
||||
public Instant expiresAt;
|
||||
public boolean irreversible;
|
||||
@@ -50,6 +51,7 @@ public class Filter extends BaseModel{
|
||||
else
|
||||
pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
if (title == null) title = phrase;
|
||||
return pattern.matcher(text).find();
|
||||
}
|
||||
|
||||
@@ -61,6 +63,7 @@ public class Filter extends BaseModel{
|
||||
public String toString(){
|
||||
return "Filter{"+
|
||||
"id='"+id+'\''+
|
||||
", title='"+title+'\''+
|
||||
", phrase='"+phrase+'\''+
|
||||
", context="+context+
|
||||
", expiresAt="+expiresAt+
|
||||
@@ -77,7 +80,9 @@ public class Filter extends BaseModel{
|
||||
@SerializedName("public")
|
||||
PUBLIC,
|
||||
@SerializedName("thread")
|
||||
THREAD
|
||||
THREAD,
|
||||
@SerializedName("account")
|
||||
ACCOUNT
|
||||
}
|
||||
|
||||
public enum FilterAction{
|
||||
|
||||
@@ -45,7 +45,7 @@ public class Instance extends BaseModel{
|
||||
@RequiredField
|
||||
public String version;
|
||||
/**
|
||||
* Primary langauges of the website and its staff.
|
||||
* Primary languages of the website and its staff.
|
||||
*/
|
||||
// @RequiredField
|
||||
public List<String> languages;
|
||||
@@ -84,6 +84,8 @@ public class Instance extends BaseModel{
|
||||
|
||||
public V2 v2;
|
||||
|
||||
public Pleroma pleroma;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
@@ -193,4 +195,9 @@ public class Instance extends BaseModel{
|
||||
public boolean enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class Pleroma extends BaseModel {
|
||||
// metadata etc
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ public class Notification extends BaseModel implements DisplayItemsParent{
|
||||
public Instant createdAt;
|
||||
@RequiredField
|
||||
public Account account;
|
||||
|
||||
public Status status;
|
||||
public Report report;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
@@ -50,6 +50,17 @@ public class Notification extends BaseModel implements DisplayItemsParent{
|
||||
@SerializedName("status")
|
||||
STATUS,
|
||||
@SerializedName("update")
|
||||
UPDATE
|
||||
UPDATE,
|
||||
@SerializedName("admin.sign_up")
|
||||
SIGN_UP,
|
||||
@SerializedName("admin.report")
|
||||
REPORT
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class Report {
|
||||
public String id;
|
||||
public String comment;
|
||||
public Account targetAccount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ParsedAccount{
|
||||
public Account account;
|
||||
public CharSequence parsedName, parsedBio;
|
||||
public CustomEmojiHelper emojiHelper;
|
||||
public ImageLoaderRequest avatarRequest;
|
||||
|
||||
public ParsedAccount(Account account, String accountID){
|
||||
this.account=account;
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
|
||||
ssb.append(parsedBio);
|
||||
emojiHelper.setText(ssb);
|
||||
|
||||
avatarRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(40), V.dp(40));
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,11 @@ public class PushNotification extends BaseModel{
|
||||
@SerializedName("status")
|
||||
STATUS(R.string.sk_notification_type_status),
|
||||
@SerializedName("update")
|
||||
UPDATE(R.string.sk_notification_type_update);
|
||||
UPDATE(R.string.sk_notification_type_update),
|
||||
@SerializedName("admin.sign_up")
|
||||
SIGN_UP(R.string.sk_sign_ups),
|
||||
@SerializedName("admin.report")
|
||||
REPORT(R.string.sk_new_reports);
|
||||
|
||||
@StringRes
|
||||
public final int localizedName;
|
||||
|
||||
@@ -47,6 +47,14 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
||||
public boolean status;
|
||||
public boolean update;
|
||||
|
||||
// set to true here because i didn't add any items for those to the settings
|
||||
// (so i don't have to determine whether the user is an admin to show the items or not, and
|
||||
// admins can still disable those through the android notifications settings)
|
||||
@SerializedName("admin.sign_up")
|
||||
public boolean adminSignUp = true;
|
||||
@SerializedName("admin.report")
|
||||
public boolean adminReport = true;
|
||||
|
||||
public static Alerts ofAll(){
|
||||
Alerts alerts=new Alerts();
|
||||
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=alerts.status=alerts.update=true;
|
||||
@@ -63,6 +71,8 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
||||
", poll="+poll+
|
||||
", status="+status+
|
||||
", update="+update+
|
||||
", adminSignUp="+adminSignUp+
|
||||
", adminReport="+adminReport+
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ public class Relationship extends BaseModel{
|
||||
public boolean blockedBy;
|
||||
public String note;
|
||||
|
||||
public boolean canFollow(){
|
||||
return !(following || blocking || blockedBy || domainBlocking);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Relationship{"+
|
||||
|
||||
@@ -62,19 +62,13 @@ public class ScheduledStatus extends BaseModel implements DisplayItemsParent{
|
||||
}
|
||||
|
||||
public Status toStatus() {
|
||||
Status s = new Status();
|
||||
s.id = id;
|
||||
Status s = Status.ofFake(id, params.text, scheduledAt);
|
||||
s.mediaAttachments = mediaAttachments;
|
||||
s.createdAt = scheduledAt;
|
||||
s.inReplyToId = params.inReplyToId > 0 ? "" + params.inReplyToId : null;
|
||||
s.content = s.text = params.text;
|
||||
s.spoilerText = params.spoilerText;
|
||||
s.visibility = params.visibility;
|
||||
s.language = params.language;
|
||||
s.sensitive = params.sensitive;
|
||||
s.mentions = List.of();
|
||||
s.tags = List.of();
|
||||
s.emojis = List.of();
|
||||
if (params.poll != null) s.poll = params.poll.toPoll();
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
public Card card;
|
||||
public String language;
|
||||
public String text;
|
||||
public boolean localOnly;
|
||||
|
||||
public boolean favourited;
|
||||
public boolean reblogged;
|
||||
@@ -57,7 +58,9 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
public boolean bookmarked;
|
||||
public boolean pinned;
|
||||
|
||||
public transient boolean filterRevealed;
|
||||
public transient boolean spoilerRevealed;
|
||||
public transient boolean textExpanded, textExpandable;
|
||||
public transient boolean hasGapAfter;
|
||||
private transient String strippedText;
|
||||
|
||||
@@ -83,6 +86,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
reblog.postprocess();
|
||||
|
||||
spoilerRevealed=GlobalUserPreferences.alwaysExpandContentWarnings || !sensitive;
|
||||
if (visibility.equals(StatusPrivacy.LOCAL)) localOnly = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -144,4 +148,19 @@ public class Status extends BaseModel implements DisplayItemsParent{
|
||||
strippedText=HtmlParser.strip(content);
|
||||
return strippedText;
|
||||
}
|
||||
|
||||
public static Status ofFake(String id, String text, Instant createdAt) {
|
||||
Status s = new Status();
|
||||
s.id = id;
|
||||
s.mediaAttachments = List.of();
|
||||
s.createdAt = createdAt;
|
||||
s.content = s.text = text;
|
||||
s.spoilerText = "";
|
||||
s.visibility = StatusPrivacy.PUBLIC;
|
||||
s.mentions = List.of();
|
||||
s.tags = List.of();
|
||||
s.emojis = List.of();
|
||||
s.filtered = List.of();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ public enum StatusPrivacy{
|
||||
@SerializedName("private")
|
||||
PRIVATE(2),
|
||||
@SerializedName("direct")
|
||||
DIRECT(3);
|
||||
DIRECT(3),
|
||||
@SerializedName("local")
|
||||
LOCAL(4); // akkoma
|
||||
|
||||
private int privacy;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public class TimelineDefinition {
|
||||
private @Nullable String hashtagName;
|
||||
|
||||
public static TimelineDefinition ofList(String listId, String listTitle) {
|
||||
TimelineDefinition def = new TimelineDefinition(TimelineType.LIST, listTitle);
|
||||
TimelineDefinition def = new TimelineDefinition(TimelineType.LIST);
|
||||
def.listId = listId;
|
||||
def.listTitle = listTitle;
|
||||
return def;
|
||||
@@ -42,7 +42,7 @@ public class TimelineDefinition {
|
||||
}
|
||||
|
||||
public static TimelineDefinition ofHashtag(String hashtag) {
|
||||
TimelineDefinition def = new TimelineDefinition(TimelineType.HASHTAG, hashtag);
|
||||
TimelineDefinition def = new TimelineDefinition(TimelineType.HASHTAG);
|
||||
def.hashtagName = hashtag;
|
||||
return def;
|
||||
}
|
||||
@@ -58,11 +58,6 @@ public class TimelineDefinition {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public TimelineDefinition(TimelineType type, String title) {
|
||||
this.type = type;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getTitle(Context ctx) {
|
||||
return title != null ? title : getDefaultTitle(ctx);
|
||||
}
|
||||
@@ -142,7 +137,8 @@ public class TimelineDefinition {
|
||||
}
|
||||
|
||||
public TimelineDefinition copy() {
|
||||
TimelineDefinition def = new TimelineDefinition(type, title);
|
||||
TimelineDefinition def = new TimelineDefinition(type);
|
||||
def.title = title;
|
||||
def.listId = listId;
|
||||
def.listTitle = listTitle;
|
||||
def.hashtagName = hashtagName;
|
||||
@@ -222,7 +218,7 @@ public class TimelineDefinition {
|
||||
LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),
|
||||
FEDERATED(R.drawable.ic_fluent_earth_24_regular, R.string.sk_timeline_federated, true),
|
||||
POST_NOTIFICATIONS(R.drawable.ic_fluent_chat_24_regular, R.string.sk_timeline_posts, true),
|
||||
LIST(R.drawable.ic_fluent_people_list_24_regular, R.string.sk_list, true),
|
||||
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
|
||||
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true);
|
||||
|
||||
public final int iconRes, nameRes;
|
||||
|
||||
@@ -96,9 +96,10 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
visibility.setImageResource(switch (s.visibility) {
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
|
||||
case DIRECT -> R.drawable.ic_fluent_mention_20_regular;
|
||||
case LOCAL -> R.drawable.ic_fluent_eye_20_regular;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
boost.setSelected(item.status.reblogged);
|
||||
favorite.setSelected(item.status.favourited);
|
||||
bookmark.setSelected(item.status.bookmarked);
|
||||
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
|
||||
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED || item.status.visibility==StatusPrivacy.LOCAL
|
||||
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
|
||||
}
|
||||
|
||||
@@ -239,8 +239,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
Drawable checkMark = ctx.getDrawable(R.drawable.ic_fluent_checkmark_circle_20_regular);
|
||||
Drawable publicDrawable = ctx.getDrawable(R.drawable.ic_fluent_earth_24_regular);
|
||||
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_community_24_regular);
|
||||
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_people_checkmark_24_regular);
|
||||
Drawable unlistedDrawable = ctx.getDrawable(R.drawable.ic_fluent_lock_open_24_regular);
|
||||
Drawable followersDrawable = ctx.getDrawable(R.drawable.ic_fluent_lock_closed_24_regular);
|
||||
|
||||
StatusPrivacy defaultVisibility = session.preferences != null ? session.preferences.postingDefaultVisibility : null;
|
||||
// e.g. post visibility is unlisted, but default is public
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
@@ -21,6 +20,8 @@ import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
@@ -43,6 +44,7 @@ import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -52,6 +54,7 @@ import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -136,7 +139,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView name, username, timestamp, extraText, separator;
|
||||
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator;
|
||||
private final View collapseBtn;
|
||||
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, collapseBtnIcon;
|
||||
private final PopupMenu optionsMenu;
|
||||
private Relationship relationship;
|
||||
private APIRequest<?> currentRelationshipRequest;
|
||||
@@ -159,6 +163,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
visibility=findViewById(R.id.visibility);
|
||||
deleteNotification=findViewById(R.id.delete_notification);
|
||||
unreadIndicator=findViewById(R.id.unread_indicator);
|
||||
collapseBtn=findViewById(R.id.collapse_btn);
|
||||
collapseBtnIcon=findViewById(R.id.collapse_btn_icon);
|
||||
extraText=findViewById(R.id.extra_text);
|
||||
avatar.setOnClickListener(this::onAvaClick);
|
||||
avatar.setOutlineProvider(roundCornersOutline);
|
||||
@@ -170,6 +176,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
fragment.removeNotification(item.notification);
|
||||
}
|
||||
}));
|
||||
collapseBtn.setOnClickListener(l -> item.parentFragment.onToggleExpanded(item.status, getItemID()));
|
||||
|
||||
optionsMenu=new PopupMenu(activity, more);
|
||||
optionsMenu.inflate(R.menu.post);
|
||||
@@ -299,7 +306,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
timestamp.setText(item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter));
|
||||
}
|
||||
else if ((item.status==null || item.status.editedAt==null) && item.createdAt != null)
|
||||
else if ((!item.inset || item.status==null || item.status.editedAt==null) && item.createdAt != null)
|
||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||
else if (item.status != null && item.status.editedAt != null)
|
||||
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
|
||||
@@ -318,12 +325,15 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
|
||||
if(TextUtils.isEmpty(item.extraText)){
|
||||
extraText.setVisibility(View.GONE);
|
||||
if (item.status != null) {
|
||||
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), extraText, item.status.visibility, item.status.localOnly);
|
||||
}
|
||||
}else{
|
||||
extraText.setVisibility(View.VISIBLE);
|
||||
extraText.setText(item.extraText);
|
||||
}
|
||||
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
|
||||
more.setVisibility(item.inset || (item.notification != null && item.notification.report != null)
|
||||
? View.GONE : View.VISIBLE);
|
||||
avatar.setClickable(!item.inset);
|
||||
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
|
||||
if(currentRelationshipRequest!=null){
|
||||
@@ -351,6 +361,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onSuccess(Object o) {
|
||||
item.consumeReadAnnouncement.accept(item.announcement.id);
|
||||
item.announcement.read = true;
|
||||
if (item.parentFragment.getActivity() == null) return;
|
||||
rebind();
|
||||
}
|
||||
|
||||
@@ -368,6 +379,17 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
more.setContentDescription(desc);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) more.setTooltipText(desc);
|
||||
|
||||
if (item.status == null || !item.status.textExpandable) {
|
||||
collapseBtn.setVisibility(View.GONE);
|
||||
} else {
|
||||
String collapseText = item.parentFragment.getString(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
|
||||
collapseBtn.setVisibility(item.status.textExpandable ? View.VISIBLE : View.GONE);
|
||||
collapseBtn.setContentDescription(collapseText);
|
||||
if (GlobalUserPreferences.reduceMotion) collapseBtnIcon.setScaleY(item.status.textExpanded ? -1 : 1);
|
||||
else collapseBtnIcon.animate().scaleY(item.status.textExpanded ? -1 : 1).start();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) collapseBtn.setTooltipText(collapseText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -423,6 +445,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void updateOptionsMenu(){
|
||||
if (item.parentFragment.getActivity() == null) return;
|
||||
if (item.announcement != null) return;
|
||||
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
|
||||
Menu menu=optionsMenu.getMenu();
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
@@ -19,6 +28,7 @@ import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
|
||||
public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
|
||||
public final int index;
|
||||
@@ -56,11 +66,35 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
|
||||
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
|
||||
private boolean didClear;
|
||||
|
||||
private AnimatorSet currentAnim;
|
||||
private final FrameLayout altTextWrapper;
|
||||
private final TextView altTextButton;
|
||||
private final ImageView noAltTextButton;
|
||||
private final View altTextScroller;
|
||||
private final ImageButton altTextClose;
|
||||
private final TextView altText, noAltText;
|
||||
|
||||
private View altOrNoAltButton;
|
||||
private boolean altTextShown;
|
||||
|
||||
public Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
|
||||
super(activity, layout, parent);
|
||||
photo=findViewById(R.id.photo);
|
||||
photo.setOnClickListener(this::onViewClick);
|
||||
this.layout=(ImageAttachmentFrameLayout)itemView;
|
||||
|
||||
altTextWrapper=findViewById(R.id.alt_text_wrapper);
|
||||
altTextButton=findViewById(R.id.alt_button);
|
||||
noAltTextButton=findViewById(R.id.no_alt_button);
|
||||
altTextScroller=findViewById(R.id.alt_text_scroller);
|
||||
altTextClose=findViewById(R.id.alt_text_close);
|
||||
altText=findViewById(R.id.alt_text);
|
||||
noAltText=findViewById(R.id.no_alt_text);
|
||||
|
||||
altTextButton.setOnClickListener(this::onShowHideClick);
|
||||
noAltTextButton.setOnClickListener(this::onShowHideClick);
|
||||
altTextClose.setOnClickListener(this::onShowHideClick);
|
||||
// altTextScroller.setNestedScrollingEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,6 +107,111 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
photo.setContentDescription(TextUtils.isEmpty(item.attachment.description) ? item.parentFragment.getString(R.string.media_no_description) : item.attachment.description);
|
||||
didClear=false;
|
||||
|
||||
if (currentAnim != null) currentAnim.cancel();
|
||||
|
||||
boolean altTextMissing = TextUtils.isEmpty(item.attachment.description);
|
||||
altOrNoAltButton = altTextMissing ? noAltTextButton : altTextButton;
|
||||
altTextShown=false;
|
||||
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
noAltTextButton.setVisibility(View.VISIBLE);
|
||||
altTextButton.setAlpha(1f);
|
||||
noAltTextButton.setAlpha(1f);
|
||||
altTextWrapper.setVisibility(View.VISIBLE);
|
||||
|
||||
if (altTextMissing){
|
||||
if (GlobalUserPreferences.showNoAltIndicator) {
|
||||
noAltTextButton.setVisibility(View.VISIBLE);
|
||||
noAltText.setVisibility(View.VISIBLE);
|
||||
altTextWrapper.setBackgroundResource(R.drawable.bg_image_no_alt_overlay);
|
||||
altTextButton.setVisibility(View.GONE);
|
||||
altText.setVisibility(View.GONE);
|
||||
} else {
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
}
|
||||
}else{
|
||||
if (GlobalUserPreferences.showAltIndicator) {
|
||||
noAltTextButton.setVisibility(View.GONE);
|
||||
noAltText.setVisibility(View.GONE);
|
||||
altTextWrapper.setBackgroundResource(R.drawable.bg_image_alt_overlay);
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
altTextButton.setText(R.string.sk_alt_button);
|
||||
altText.setVisibility(View.VISIBLE);
|
||||
altText.setText(item.attachment.description);
|
||||
altText.setPadding(0, 0, 0, 0);
|
||||
} else {
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onShowHideClick(View v){
|
||||
boolean show=v.getId()==R.id.alt_button || v.getId()==R.id.no_alt_button;
|
||||
|
||||
if(altTextShown==show)
|
||||
return;
|
||||
if(currentAnim!=null)
|
||||
currentAnim.cancel();
|
||||
|
||||
altTextShown=show;
|
||||
if(show){
|
||||
altTextScroller.setVisibility(View.VISIBLE);
|
||||
altTextClose.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
altOrNoAltButton.setVisibility(View.VISIBLE);
|
||||
// Hide these views temporarily so FrameLayout measures correctly
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// This is the current size...
|
||||
int prevLeft=altTextWrapper.getLeft();
|
||||
int prevRight=altTextWrapper.getRight();
|
||||
int prevTop=altTextWrapper.getTop();
|
||||
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
// ...and this is after the layout pass, right now the FrameLayout has its final size, but we animate that change
|
||||
if(!show){
|
||||
// Show these views again so they're visible for the duration of the animation.
|
||||
// No one would notice they were missing during measure/layout.
|
||||
altTextScroller.setVisibility(View.VISIBLE);
|
||||
altTextClose.setVisibility(View.VISIBLE);
|
||||
}
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(altTextWrapper, "left", prevLeft, altTextWrapper.getLeft()),
|
||||
ObjectAnimator.ofInt(altTextWrapper, "right", prevRight, altTextWrapper.getRight()),
|
||||
ObjectAnimator.ofInt(altTextWrapper, "top", prevTop, altTextWrapper.getTop()),
|
||||
ObjectAnimator.ofFloat(altOrNoAltButton, View.ALPHA, show ? 1f : 0f, show ? 0f : 1f),
|
||||
ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f),
|
||||
ObjectAnimator.ofFloat(altTextClose, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
if(show){
|
||||
altOrNoAltButton.setVisibility(View.GONE);
|
||||
}else{
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
}
|
||||
currentAnim=null;
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
currentAnim=set;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
@@ -37,141 +38,8 @@ public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends ImageStatusDisplayItem.Holder<PhotoStatusDisplayItem> {
|
||||
private final FrameLayout altTextWrapper, altTextOpen;
|
||||
private final TextView altTextButton;
|
||||
private final ImageView noAltTextButton;
|
||||
private final View altTextScroller;
|
||||
private final ImageButton altTextClose;
|
||||
private final TextView altText;
|
||||
|
||||
private View altOrNoAltButton;
|
||||
private boolean altTextShown;
|
||||
private AnimatorSet currentAnim;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent) {
|
||||
super(activity, R.layout.display_item_photo, parent);
|
||||
altTextWrapper=findViewById(R.id.alt_text_wrapper);
|
||||
altTextOpen=findViewById(R.id.alt_text_open);
|
||||
altTextButton=findViewById(R.id.alt_button);
|
||||
noAltTextButton=findViewById(R.id.no_alt_button);
|
||||
altTextScroller=findViewById(R.id.alt_text_scroller);
|
||||
altTextClose=findViewById(R.id.alt_text_close);
|
||||
altText=findViewById(R.id.alt_text);
|
||||
|
||||
altTextClose.setOnClickListener(this::onShowHideClick);
|
||||
// altTextScroller.setNestedScrollingEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ImageStatusDisplayItem item){
|
||||
super.onBind(item);
|
||||
boolean altTextMissing = TextUtils.isEmpty(item.attachment.description);
|
||||
altOrNoAltButton = altTextMissing ? noAltTextButton : altTextButton;
|
||||
altTextShown=false;
|
||||
if(currentAnim!=null)
|
||||
currentAnim.cancel();
|
||||
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
noAltTextButton.setVisibility(View.VISIBLE);
|
||||
altTextButton.setAlpha(1f);
|
||||
noAltTextButton.setAlpha(1f);
|
||||
altTextWrapper.setVisibility(View.VISIBLE);
|
||||
altTextOpen.setOnClickListener(this::onShowHideClick);
|
||||
|
||||
if (altTextMissing){
|
||||
if (GlobalUserPreferences.showNoAltIndicator) {
|
||||
noAltTextButton.setVisibility(View.VISIBLE);
|
||||
altTextWrapper.setBackgroundResource(R.drawable.bg_image_no_alt_overlay);
|
||||
altTextButton.setVisibility(View.GONE);
|
||||
altText.setText(R.string.sk_no_alt_text);
|
||||
altText.setPadding(V.dp(8), 0, 0, 0);
|
||||
} else {
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
}
|
||||
}else{
|
||||
if (GlobalUserPreferences.showAltIndicator) {
|
||||
noAltTextButton.setVisibility(View.GONE);
|
||||
altTextWrapper.setBackgroundResource(R.drawable.bg_image_alt_overlay);
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
altTextButton.setText(R.string.sk_alt_button);
|
||||
altText.setText(item.attachment.description);
|
||||
altText.setPadding(0, 0, 0, 0);
|
||||
} else {
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onShowHideClick(View v){
|
||||
boolean show=v.getId()==R.id.alt_text_open;
|
||||
|
||||
altTextOpen.setOnClickListener(show ? null : this::onShowHideClick);
|
||||
|
||||
if(altTextShown==show)
|
||||
return;
|
||||
if(currentAnim!=null)
|
||||
currentAnim.cancel();
|
||||
|
||||
altTextShown=show;
|
||||
if(show){
|
||||
altTextScroller.setVisibility(View.VISIBLE);
|
||||
altTextClose.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
altOrNoAltButton.setVisibility(View.VISIBLE);
|
||||
// Hide these views temporarily so FrameLayout measures correctly
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// This is the current size...
|
||||
int prevLeft=altTextWrapper.getLeft();
|
||||
int prevRight=altTextWrapper.getRight();
|
||||
int prevBottom=altTextWrapper.getBottom();
|
||||
int prevTop=altTextOpen.getTop();
|
||||
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
// ...and this is after the layout pass, right now the FrameLayout has its final size, but we animate that change
|
||||
if(!show){
|
||||
// Show these views again so they're visible for the duration of the animation.
|
||||
// No one would notice they were missing during measure/layout.
|
||||
altTextScroller.setVisibility(View.VISIBLE);
|
||||
altTextClose.setVisibility(View.VISIBLE);
|
||||
}
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(altTextWrapper, "left", prevLeft, altTextWrapper.getLeft()),
|
||||
ObjectAnimator.ofInt(altTextWrapper, "right", prevRight, altTextWrapper.getRight()),
|
||||
ObjectAnimator.ofInt(altTextWrapper, "bottom", prevBottom, altTextWrapper.getBottom()),
|
||||
ObjectAnimator.ofInt(altTextOpen, "top", prevTop, altTextOpen.getTop()),
|
||||
ObjectAnimator.ofFloat(altOrNoAltButton, View.ALPHA, show ? 1f : 0f, show ? 0f : 1f),
|
||||
ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f),
|
||||
ObjectAnimator.ofFloat(altTextClose, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
if(show){
|
||||
altOrNoAltButton.setVisibility(View.GONE);
|
||||
}else{
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
}
|
||||
currentAnim=null;
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
currentAnim=set;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,11 @@ public class PollFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
@Override
|
||||
public void onBind(PollFooterStatusDisplayItem item){
|
||||
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_voters, item.poll.votersCount, item.poll.votersCount);
|
||||
String sep=item.parentFragment.getString(R.string.sk_separator);
|
||||
if(item.poll.expiresAt!=null && !item.poll.isExpired()){
|
||||
text+=" · "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
|
||||
text+=" "+sep+" "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
|
||||
}else if(item.poll.isExpired()){
|
||||
text+=" · "+item.parentFragment.getString(R.string.poll_closed);
|
||||
text+=" "+sep+" "+item.parentFragment.getString(R.string.poll_closed);
|
||||
}
|
||||
this.text.setText(text);
|
||||
button.setVisibility(item.poll.isExpired() || item.poll.voted || (!item.poll.multiple && !GlobalUserPreferences.voteButtonForSingleChoice) ? View.GONE : View.VISIBLE);
|
||||
|
||||
@@ -55,8 +55,8 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
this.visibility = visibility;
|
||||
this.iconEnd = visibility != null ? switch (visibility) {
|
||||
case PUBLIC -> R.drawable.ic_fluent_earth_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_people_community_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_people_checkmark_20_regular;
|
||||
case UNLISTED -> R.drawable.ic_fluent_lock_open_20_regular;
|
||||
case PRIVATE -> R.drawable.ic_fluent_lock_closed_20_filled;
|
||||
default -> 0;
|
||||
} : 0;
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.HomeTabFragment;
|
||||
import org.joinmastodon.android.fragments.HomeTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
@@ -25,6 +27,7 @@ import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -78,21 +81,39 @@ public abstract class StatusDisplayItem{
|
||||
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
||||
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
|
||||
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
|
||||
};
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false);
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, Filter.FilterContext.HOME);
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, disableTranslate, Filter.FilterContext.HOME);
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){
|
||||
String parentID=parentObject.getID();
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
|
||||
Status statusForContent=status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
|
||||
|
||||
List<Filter> filters = AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream()
|
||||
.filter(f -> f.context.contains(filterContext)).collect(Collectors.toList());
|
||||
StatusFilterPredicate filterPredicate = new StatusFilterPredicate(filters);
|
||||
|
||||
if(!statusForContent.filterRevealed){
|
||||
statusForContent.filterRevealed = filterPredicate.testWithWarning(status);
|
||||
}
|
||||
|
||||
if(status.reblog!=null){
|
||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{
|
||||
@@ -105,8 +126,11 @@ public abstract class StatusDisplayItem{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}));
|
||||
} else if (!(status.tags.isEmpty() || fragment instanceof HashtagTimelineFragment) &&
|
||||
fragment.getParentFragment() instanceof HomeTabFragment home
|
||||
} else if (
|
||||
!(status.tags.isEmpty() ||
|
||||
fragment instanceof HashtagTimelineFragment ||
|
||||
fragment instanceof ListTimelineFragment
|
||||
) && fragment.getParentFragment() instanceof HomeTabFragment home
|
||||
) {
|
||||
home.getHashtags().stream()
|
||||
.filter(followed -> status.tags.stream()
|
||||
@@ -166,6 +190,13 @@ public abstract class StatusDisplayItem{
|
||||
item.inset=inset;
|
||||
item.index=i++;
|
||||
}
|
||||
|
||||
if (!statusForContent.filterRevealed) {
|
||||
return new ArrayList<>(List.of(
|
||||
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)
|
||||
));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -192,6 +223,7 @@ public abstract class StatusDisplayItem{
|
||||
ACCOUNT,
|
||||
HASHTAG,
|
||||
GAP,
|
||||
WARNING,
|
||||
EXTENDED_FOOTER
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,17 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Button;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.bottomSoftwareFoundation.bottom.Bottom;
|
||||
import com.github.bottomSoftwareFoundation.bottom.TranslationError;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -20,13 +23,15 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.model.TranslatedStatus;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
import org.joinmastodon.android.utils.StatusTextEncoder;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -46,6 +51,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean translated = false;
|
||||
public TranslatedStatus translation = null;
|
||||
private AccountSession session;
|
||||
public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48");
|
||||
|
||||
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){
|
||||
super(parentID, parentFragment);
|
||||
@@ -83,10 +89,14 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final LinkedTextView text;
|
||||
private final LinearLayout spoilerHeader;
|
||||
private final TextView spoilerTitle, spoilerTitleInline, translateInfo;
|
||||
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress;
|
||||
private final TextView spoilerTitle, spoilerTitleInline, translateInfo, readMore;
|
||||
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress, spaceBelowText;
|
||||
private final int backgroundColor, borderColor;
|
||||
private final Button translateButton;
|
||||
private final ScrollView textScrollView;
|
||||
|
||||
private final float textMaxHeight, textCollapsedHeight;
|
||||
private final LinearLayout.LayoutParams collapseParams, wrapParams;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_text, parent);
|
||||
@@ -105,6 +115,14 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||
backgroundColor=UiUtils.getThemeColor(activity, R.attr.colorBackgroundLight);
|
||||
borderColor=UiUtils.getThemeColor(activity, R.attr.colorPollVoted);
|
||||
textScrollView=findViewById(R.id.text_scroll_view);
|
||||
readMore=findViewById(R.id.read_more);
|
||||
spaceBelowText=findViewById(R.id.space_below_text);
|
||||
textMaxHeight=activity.getResources().getDimension(R.dimen.text_max_height);
|
||||
textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height);
|
||||
collapseParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) textCollapsedHeight);
|
||||
wrapParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
readMore.setOnClickListener(v -> item.parentFragment.onToggleExpanded(item.status, getItemID()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,6 +131,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
? HtmlParser.parse(item.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID())
|
||||
: item.text);
|
||||
text.setTextIsSelectable(item.textSelectable);
|
||||
if (item.textSelectable) {
|
||||
textScrollView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
spoilerTitleInline.setTextIsSelectable(item.textSelectable);
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
spoilerTitleInline.setBackgroundColor(item.inset ? 0 : backgroundColor);
|
||||
@@ -141,20 +162,34 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(item.session.domain);
|
||||
boolean translateEnabled = !item.disableTranslate && instanceInfo.v2 != null &&
|
||||
instanceInfo.v2.configuration.translation != null && instanceInfo.v2.configuration.translation.enabled;
|
||||
boolean translateEnabled = !item.disableTranslate && instanceInfo != null &&
|
||||
instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null &&
|
||||
instanceInfo.v2.configuration.translation.enabled;
|
||||
|
||||
translateWrap.setVisibility(
|
||||
(!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable) &&
|
||||
boolean isBottomText = BOTTOM_TEXT_PATTERN.matcher(item.status.getStrippedText()).find();
|
||||
boolean translateVisible = (isBottomText || (
|
||||
translateEnabled &&
|
||||
!item.status.visibility.isLessVisibleThan(StatusPrivacy.UNLISTED) &&
|
||||
item.status.language != null &&
|
||||
(item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage))
|
||||
? View.VISIBLE : View.GONE);
|
||||
(item.session.preferences == null || !item.status.language.equalsIgnoreCase(item.session.preferences.postingDefaultLanguage))))
|
||||
&& (!GlobalUserPreferences.translateButtonOpenedOnly || item.textSelectable);
|
||||
translateWrap.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
|
||||
translateButton.setText(item.translated ? R.string.sk_translate_show_original : R.string.sk_translate_post);
|
||||
translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, item.translation.provider) : "");
|
||||
translateInfo.setText(item.translated ? itemView.getResources().getString(R.string.sk_translated_using, isBottomText ? "bottom-java" : item.translation.provider) : "");
|
||||
translateButton.setOnClickListener(v->{
|
||||
if (item.translation == null) {
|
||||
if (isBottomText) {
|
||||
try {
|
||||
item.translation = new TranslatedStatus();
|
||||
item.translation.content = new StatusTextEncoder(Bottom::decode).decode(item.status.getStrippedText(), BOTTOM_TEXT_PATTERN);
|
||||
item.translated = true;
|
||||
} catch (TranslationError err) {
|
||||
item.translation = null;
|
||||
Toast.makeText(itemView.getContext(), err.getLocalizedMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
rebind();
|
||||
return;
|
||||
}
|
||||
translateProgress.setVisibility(View.VISIBLE);
|
||||
translateButton.setClickable(false);
|
||||
translateButton.animate().alpha(0.5f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(150).start();
|
||||
@@ -163,6 +198,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onSuccess(TranslatedStatus translatedStatus) {
|
||||
item.translation = translatedStatus;
|
||||
item.translated = true;
|
||||
if (item.parentFragment.getActivity() == null) return;
|
||||
translateProgress.setVisibility(View.GONE);
|
||||
translateButton.setClickable(true);
|
||||
translateButton.animate().alpha(1).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(50).start();
|
||||
@@ -182,6 +218,26 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
rebind();
|
||||
}
|
||||
});
|
||||
|
||||
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
|
||||
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (!GlobalUserPreferences.collapseLongPosts) {
|
||||
textScrollView.setLayoutParams(wrapParams);
|
||||
readMore.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (GlobalUserPreferences.collapseLongPosts) text.post(() -> {
|
||||
boolean tooBig = text.getMeasuredHeight() > textMaxHeight;
|
||||
boolean inTimeline = !item.textSelectable;
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText);
|
||||
boolean expandable = inTimeline && tooBig && !hasSpoiler;
|
||||
item.parentFragment.onEnableExpandable(this, expandable);
|
||||
});
|
||||
|
||||
readMore.setVisibility(item.status.textExpandable && !item.status.textExpanded ? View.VISIBLE : View.GONE);
|
||||
textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams);
|
||||
if (item.status.textExpandable && !translateVisible) spaceBelowText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean loading;
|
||||
public final Status status;
|
||||
public List<StatusDisplayItem> filteredItems;
|
||||
|
||||
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.WARNING;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<WarningFilteredStatusDisplayItem>{
|
||||
public final View warningWrap;
|
||||
public final TextView text;
|
||||
public List<StatusDisplayItem> filteredItems;
|
||||
|
||||
public Holder(Context context, ViewGroup parent) {
|
||||
super(context, R.layout.display_item_filter_warning, parent);
|
||||
warningWrap=findViewById(R.id.warning_wrap);
|
||||
text=findViewById(R.id.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(WarningFilteredStatusDisplayItem item) {
|
||||
filteredItems = item.filteredItems;
|
||||
text.setText(item.parentFragment.getString(R.string.sk_filtered, item.status.filtered.get(item.status.filtered.size() -1).filter.title));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
item.parentFragment.onWarningClick(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,7 +602,7 @@ public class TabLayout extends HorizontalScrollView {
|
||||
* <p>If the tab indicator color is not {@code Color.TRANSPARENT}, the indicator will be wrapped
|
||||
* and tinted right before it is drawn by {@link SlidingTabIndicator#draw(Canvas)}. If you'd like
|
||||
* the inherent color or the tinted color of a custom drawable to be used, make sure this color is
|
||||
* set to {@code Color.TRANSPARENT} to avoid your color/tint being overriden.
|
||||
* set to {@code Color.TRANSPARENT} to avoid your color/tint being overridden.
|
||||
*
|
||||
* @param color color to use for the indicator
|
||||
* @attr ref com.google.android.material.R.styleable#TabLayout_tabIndicatorColor
|
||||
|
||||
@@ -16,6 +16,10 @@ public class LinkSpan extends CharacterStyle {
|
||||
private String accountID;
|
||||
private String text;
|
||||
|
||||
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID){
|
||||
this(link, listener, type, accountID, null);
|
||||
}
|
||||
|
||||
public LinkSpan(String link, OnLinkClickListener listener, Type type, String accountID, String text){
|
||||
this.listener=listener;
|
||||
this.link=link;
|
||||
@@ -38,6 +42,7 @@ public class LinkSpan extends CharacterStyle {
|
||||
case URL -> UiUtils.openURL(context, accountID, link);
|
||||
case MENTION -> UiUtils.openProfileByID(context, accountID, link);
|
||||
case HASHTAG -> UiUtils.openHashtagTimeline(context, accountID, link, null);
|
||||
case CUSTOM -> listener.onLinkClick(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +69,7 @@ public class LinkSpan extends CharacterStyle {
|
||||
public enum Type{
|
||||
URL,
|
||||
MENTION,
|
||||
HASHTAG
|
||||
HASHTAG,
|
||||
CUSTOM
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.os.ext.SdkExtensions;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
@@ -40,6 +43,8 @@ import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@@ -73,18 +78,17 @@ import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.parceler.Parcels;
|
||||
@@ -99,6 +103,7 @@ import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -130,7 +135,8 @@ public class UiUtils{
|
||||
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||
|
||||
private UiUtils(){}
|
||||
private UiUtils() {
|
||||
}
|
||||
|
||||
public static void launchWebBrowser(Context context, String url) {
|
||||
try {
|
||||
@@ -234,6 +240,7 @@ public class UiUtils{
|
||||
/**
|
||||
* Android 6.0 has a bug where start and end compound drawables don't get tinted.
|
||||
* This works around it by setting the tint colors directly to the drawables.
|
||||
*
|
||||
* @param textView
|
||||
*/
|
||||
public static void fixCompoundDrawableTintOnAndroid6(TextView textView) {
|
||||
@@ -260,7 +267,9 @@ public class UiUtils{
|
||||
mainHandler.removeCallbacks(runnable);
|
||||
}
|
||||
|
||||
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */
|
||||
/**
|
||||
* Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}.
|
||||
*/
|
||||
public static int lerp(int startValue, int endValue, float fraction) {
|
||||
return startValue + Math.round(fraction * (endValue - startValue));
|
||||
}
|
||||
@@ -272,7 +281,8 @@ public class UiUtils{
|
||||
String name = cursor.getString(0);
|
||||
if (name != null)
|
||||
return name;
|
||||
}catch(Throwable ignore){}
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
}
|
||||
return uri.getLastPathSegment();
|
||||
}
|
||||
@@ -374,6 +384,7 @@ public class UiUtils{
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Relationship result) {
|
||||
if (activity == null) return;
|
||||
resultCallback.accept(result);
|
||||
if (!currentlyBlocked) {
|
||||
E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
|
||||
@@ -402,6 +413,7 @@ public class UiUtils{
|
||||
new SetAccountBlocked(account.id, false).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Relationship relationship) {
|
||||
if (activity == null) return;
|
||||
Toast.makeText(activity, R.string.sk_remove_follower_success, Toast.LENGTH_SHORT).show();
|
||||
resultCallback.accept(relationship);
|
||||
}
|
||||
@@ -470,6 +482,7 @@ public class UiUtils{
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
|
||||
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback) {
|
||||
confirmDeletePost(activity, accountID, status, resultCallback, false);
|
||||
}
|
||||
@@ -509,7 +522,7 @@ public class UiUtils{
|
||||
() -> new DeleteStatus.Scheduled(status.id)
|
||||
.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object nothing){
|
||||
public void onSuccess(Object o) {
|
||||
resultCallback.run();
|
||||
E.post(new ScheduledStatusDeletedEvent(status.id, accountID));
|
||||
}
|
||||
@@ -654,6 +667,38 @@ public class UiUtils{
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButtonM3(Relationship relationship, Button button){
|
||||
boolean secondaryStyle;
|
||||
if(relationship.blocking){
|
||||
button.setText(R.string.button_blocked);
|
||||
secondaryStyle=true;
|
||||
}else if(relationship.blockedBy){
|
||||
button.setText(R.string.button_follow);
|
||||
secondaryStyle=false;
|
||||
}else if(relationship.requested){
|
||||
button.setText(R.string.button_follow_pending);
|
||||
secondaryStyle=true;
|
||||
}else if(!relationship.following){
|
||||
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
|
||||
secondaryStyle=false;
|
||||
}else{
|
||||
button.setText(R.string.button_following);
|
||||
secondaryStyle=true;
|
||||
}
|
||||
|
||||
button.setEnabled(!relationship.blockedBy);
|
||||
int styleRes=secondaryStyle ? R.style.Widget_Mastodon_M3_Button_Tonal : R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
if(relationship.blocking)
|
||||
button.setTextColor(button.getResources().getColorStateList(R.color.error_600));
|
||||
else
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback) {
|
||||
if (relationship.blocking) {
|
||||
confirmToggleBlockUser(activity, accountID, account, true, resultCallback);
|
||||
@@ -703,7 +748,8 @@ public class UiUtils{
|
||||
@Override
|
||||
public void onSuccess(Relationship rel) {
|
||||
E.post(new FollowRequestHandledEvent(accountID, false, account, rel));
|
||||
if (notificationID != null) E.post(new NotificationDeletedEvent(notificationID));
|
||||
if (notificationID != null)
|
||||
E.post(new NotificationDeletedEvent(notificationID));
|
||||
resultCallback.accept(rel);
|
||||
}
|
||||
|
||||
@@ -764,6 +810,7 @@ public class UiUtils{
|
||||
ColorStateList iconTint = ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
|
||||
insetPopupMenuIcon(item, iconTint);
|
||||
}
|
||||
|
||||
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) {
|
||||
Drawable icon = item.getIcon().mutate();
|
||||
if (Build.VERSION.SDK_INT >= 26) item.setIconTintList(iconTint);
|
||||
@@ -791,8 +838,8 @@ public class UiUtils{
|
||||
m.setAccessible(true);
|
||||
m.invoke(menu, true);
|
||||
enableMenuIcons(context, menu, asAction);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
catch(Exception ignored){}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,7 +849,8 @@ public class UiUtils{
|
||||
MenuItem item = m.getItem(i);
|
||||
SubMenu subMenu = item.getSubMenu();
|
||||
if (subMenu != null) enableMenuIcons(context, subMenu, exclude);
|
||||
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) continue;
|
||||
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId()))
|
||||
continue;
|
||||
insetPopupMenuIcon(item, iconTint);
|
||||
}
|
||||
}
|
||||
@@ -816,7 +864,8 @@ public class UiUtils{
|
||||
Method setOptionalIconsVisible = m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
|
||||
setOptionalIconsVisible.setAccessible(true);
|
||||
setOptionalIconsVisible.invoke(m, true);
|
||||
}catch(Exception ignore){}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
enableMenuIcons(context, m);
|
||||
}
|
||||
@@ -831,6 +880,7 @@ public class UiUtils{
|
||||
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
|
||||
if (palette != null) palette.apply(context);
|
||||
}
|
||||
|
||||
public static boolean isDarkTheme() {
|
||||
if (theme == GlobalUserPreferences.ThemePreference.AUTO)
|
||||
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
|
||||
@@ -861,7 +911,8 @@ public class UiUtils{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null) return false;
|
||||
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null)
|
||||
return false;
|
||||
|
||||
String it = uri.getPath();
|
||||
return it.matches("^/@[^/]+$") ||
|
||||
@@ -912,6 +963,23 @@ public class UiUtils{
|
||||
return back;
|
||||
}
|
||||
|
||||
public static boolean setExtraTextInfo(Context ctx, TextView extraText, StatusPrivacy visibility, boolean localOnly) {
|
||||
List<String> extraParts = new ArrayList<>();
|
||||
if (localOnly || (visibility != null && visibility.equals(StatusPrivacy.LOCAL)))
|
||||
extraParts.add(ctx.getString(R.string.sk_inline_local_only));
|
||||
if (visibility != null && visibility.equals(StatusPrivacy.DIRECT))
|
||||
extraParts.add(ctx.getString(R.string.sk_inline_direct));
|
||||
if (!extraParts.isEmpty()) {
|
||||
String sep = ctx.getString(R.string.sk_separator);
|
||||
extraText.setText(String.join(" " + sep + " ", extraParts));
|
||||
extraText.setVisibility(View.VISIBLE);
|
||||
return true;
|
||||
} else {
|
||||
extraText.setVisibility(View.GONE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface InteractionPerformer {
|
||||
void interact(StatusInteractionController ic, Status status, Consumer<Status> resultConsumer);
|
||||
@@ -945,7 +1013,8 @@ public class UiUtils{
|
||||
@Override
|
||||
public void onSuccess(SearchResults results) {
|
||||
if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0));
|
||||
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
||||
else
|
||||
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1017,7 +1086,8 @@ public class UiUtils{
|
||||
Nav.go((Activity) context, ProfileFragment.class, args);
|
||||
} else {
|
||||
if (launchBrowser) launchWebBrowser(context, url);
|
||||
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
||||
else
|
||||
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1050,7 +1120,8 @@ public class UiUtils{
|
||||
Class<?> props = Class.forName("android.os.SystemProperties");
|
||||
Method get = props.getMethod("get", String.class);
|
||||
return (String) get.invoke(null, key);
|
||||
}catch(Exception ignore){}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1058,6 +1129,14 @@ public class UiUtils{
|
||||
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
|
||||
}
|
||||
|
||||
public static int alphaBlendColors(int color1, int color2, float alpha) {
|
||||
float alpha0 = 1f - alpha;
|
||||
int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha);
|
||||
int g = Math.round(((color1 >> 8) & 0xFF) * alpha0 + ((color2 >> 8) & 0xFF) * alpha);
|
||||
int b = Math.round((color1 & 0xFF) * alpha0 + (color2 & 0xFF) * alpha);
|
||||
return 0xFF000000 | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
|
||||
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText) {
|
||||
Bundle args = new Bundle();
|
||||
if (prefilledText != null) args.putString("prefilledText", prefilledText);
|
||||
@@ -1094,4 +1173,101 @@ public class UiUtils{
|
||||
Log.e("reduceSwipeSensitivity", Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public static View makeOverflowActionView(Context ctx) {
|
||||
// container needs tooltip, content description
|
||||
LinearLayout container = new LinearLayout(ctx, null, 0, R.style.Widget_Mastodon_ActionButton_Overflow) {
|
||||
@Override
|
||||
public CharSequence getAccessibilityClassName() {
|
||||
return Button.class.getName();
|
||||
}
|
||||
};
|
||||
// image needs, well, the image, and the paddings
|
||||
ImageView image = new ImageView(ctx, null, 0, R.style.Widget_Mastodon_ActionButton_Overflow);
|
||||
|
||||
image.setDuplicateParentStateEnabled(true);
|
||||
image.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
image.setClickable(false);
|
||||
image.setFocusable(false);
|
||||
image.setEnabled(false);
|
||||
|
||||
// problem: as per overflow action button defaults, the padding on left and right is unequal
|
||||
// so (however the native overflow button manages this), the ripple background is off-center
|
||||
|
||||
// workaround: set both paddings to the smaller, left one…
|
||||
int end = image.getPaddingEnd();
|
||||
int start = image.getPaddingStart();
|
||||
int paddingDiff = end - start; // what's missing to the long padding
|
||||
image.setPaddingRelative(start, image.getPaddingTop(), start, image.getPaddingBottom());
|
||||
|
||||
// …and add the missing padding to the right on the container
|
||||
container.setPaddingRelative(0, 0, paddingDiff, 0);
|
||||
container.setBackground(null);
|
||||
container.setClickable(true);
|
||||
container.setFocusable(true);
|
||||
|
||||
container.addView(image);
|
||||
|
||||
// fucking finally
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if Android platform photopicker is available on the device\
|
||||
*
|
||||
* @return whether the device supports photopicker intents.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public static boolean isPhotoPickerAvailable(){
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
|
||||
return true;
|
||||
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.R){
|
||||
return SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R)>=2;
|
||||
}else
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
public static Intent getMediaPickerIntent(String[] mimeTypes, int maxCount){
|
||||
Intent intent;
|
||||
if(isPhotoPickerAvailable()){
|
||||
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
if(maxCount>1)
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, maxCount);
|
||||
}else{
|
||||
intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
}
|
||||
if(mimeTypes.length>1){
|
||||
intent.setType("*/*");
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
|
||||
}else if(mimeTypes.length==1){
|
||||
intent.setType(mimeTypes[0]);
|
||||
}else{
|
||||
intent.setType("*/*");
|
||||
}
|
||||
if(maxCount>1)
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a View.OnClickListener to filter multiple clicks in succession.
|
||||
* Useful for buttons that perform some action that changes their state asynchronously.
|
||||
* @param l
|
||||
* @return
|
||||
*/
|
||||
public static View.OnClickListener rateLimitedClickListener(View.OnClickListener l){
|
||||
return new View.OnClickListener(){
|
||||
private long lastClickTime;
|
||||
|
||||
@Override
|
||||
public void onClick(View v){
|
||||
if(SystemClock.uptimeMillis()-lastClickTime>500L){
|
||||
lastClickTime=SystemClock.uptimeMillis();
|
||||
l.onClick(v);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.text.Editable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.EditText;
|
||||
@@ -47,6 +48,7 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
private RectF tmpRect=new RectF();
|
||||
private ColorStateList labelColors, origHintColors;
|
||||
private boolean errorState;
|
||||
private TextView errorView;
|
||||
|
||||
public FloatingHintEditTextLayout(Context context){
|
||||
this(context, null);
|
||||
@@ -95,12 +97,22 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
label.setAlpha(0f);
|
||||
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged));
|
||||
|
||||
errorView=new LinkedTextView(getContext());
|
||||
errorView.setTextAppearance(R.style.m3_body_small);
|
||||
errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
errorView.setLinkTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
|
||||
errorView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
errorView.setPadding(V.dp(16), V.dp(4), V.dp(16), 0);
|
||||
errorView.setVisibility(View.GONE);
|
||||
addView(errorView);
|
||||
}
|
||||
|
||||
private void onTextChanged(Editable text){
|
||||
if(errorState){
|
||||
errorView.setVisibility(View.GONE);
|
||||
errorState=false;
|
||||
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field));
|
||||
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field, getContext().getTheme()));
|
||||
refreshDrawableState();
|
||||
}
|
||||
boolean newHintVisible=text.length()==0;
|
||||
@@ -211,12 +223,34 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
label.setTextColor(color.getColorForState(getDrawableState(), 0xff00ff00));
|
||||
}
|
||||
|
||||
public void setErrorState(){
|
||||
public void setErrorState(CharSequence error){
|
||||
if(errorState)
|
||||
return;
|
||||
errorState=true;
|
||||
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field_error, getContext().getTheme()));
|
||||
label.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Error));
|
||||
errorView.setVisibility(VISIBLE);
|
||||
errorView.setText(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
if(errorView.getVisibility()!=GONE){
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec)-getPaddingLeft()-getPaddingRight();
|
||||
LayoutParams editLP=(LayoutParams) edit.getLayoutParams();
|
||||
width-=editLP.leftMargin+editLP.rightMargin;
|
||||
errorView.measure(width | MeasureSpec.EXACTLY, MeasureSpec.UNSPECIFIED);
|
||||
LayoutParams lp=(LayoutParams) errorView.getLayoutParams();
|
||||
lp.width=width;
|
||||
lp.height=errorView.getMeasuredHeight();
|
||||
lp.gravity=Gravity.LEFT | Gravity.BOTTOM;
|
||||
lp.leftMargin=editLP.leftMargin;
|
||||
editLP.bottomMargin=errorView.getMeasuredHeight();
|
||||
}else{
|
||||
LayoutParams editLP=(LayoutParams) edit.getLayoutParams();
|
||||
editLP.bottomMargin=0;
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
private class PaddedForegroundDrawable extends Drawable{
|
||||
@@ -313,8 +347,8 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
@Override
|
||||
protected void onBoundsChange(@NonNull Rect bounds){
|
||||
super.onBoundsChange(bounds);
|
||||
LayoutParams lp=(LayoutParams) edit.getLayoutParams();
|
||||
wrapped.setBounds(bounds.left+lp.leftMargin-V.dp(12), bounds.top, bounds.right-lp.rightMargin+V.dp(12), bounds.bottom);
|
||||
int offset=V.dp(12);
|
||||
wrapped.setBounds(edit.getLeft()-offset, edit.getTop()-offset, edit.getRight()+offset, edit.getBottom()+offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
public class UntouchableScrollView extends ScrollView {
|
||||
public UntouchableScrollView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public UntouchableScrollView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public UntouchableScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public UntouchableScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
super.onTouchEvent(event);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.joinmastodon.android.utils;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class ElevationOnScrollListener extends RecyclerView.OnScrollListener implements View.OnScrollChangeListener{
|
||||
private boolean isAtTop;
|
||||
private Animator currentPanelsAnim;
|
||||
private View[] views;
|
||||
private FragmentRootLinearLayout fragmentRootLayout;
|
||||
|
||||
public ElevationOnScrollListener(FragmentRootLinearLayout fragmentRootLayout, View... views){
|
||||
isAtTop=true;
|
||||
this.fragmentRootLayout=fragmentRootLayout;
|
||||
this.views=views;
|
||||
for(View v:views){
|
||||
Drawable bg=v.getBackground().mutate();
|
||||
v.setBackground(bg);
|
||||
if(bg instanceof LayerDrawable ld){
|
||||
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
|
||||
if(overlay!=null){
|
||||
overlay.setAlpha(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setViews(View... views){
|
||||
List<View> oldViews=Arrays.asList(this.views);
|
||||
this.views=views;
|
||||
for(View v:views){
|
||||
if(oldViews.contains(v))
|
||||
continue;
|
||||
Drawable bg=v.getBackground().mutate();
|
||||
v.setBackground(bg);
|
||||
if(bg instanceof LayerDrawable ld){
|
||||
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
|
||||
if(overlay!=null){
|
||||
overlay.setAlpha(isAtTop ? 0 : 20);
|
||||
}
|
||||
}
|
||||
v.setTranslationZ(isAtTop ? 0 : V.dp(3));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
|
||||
handleScroll(recyclerView.getContext(), newAtTop);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
handleScroll(v.getContext(), scrollY<=0);
|
||||
}
|
||||
|
||||
private void handleScroll(Context context, boolean newAtTop){
|
||||
if(newAtTop!=isAtTop){
|
||||
isAtTop=newAtTop;
|
||||
if(currentPanelsAnim!=null)
|
||||
currentPanelsAnim.cancel();
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
ArrayList<Animator> anims=new ArrayList<>();
|
||||
for(View v:views){
|
||||
if(v.getBackground() instanceof LayerDrawable ld){
|
||||
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
|
||||
if(overlay!=null){
|
||||
anims.add(ObjectAnimator.ofInt(overlay, "alpha", isAtTop ? 0 : 20));
|
||||
}
|
||||
}
|
||||
anims.add(ObjectAnimator.ofFloat(v, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3)));
|
||||
}
|
||||
if(fragmentRootLayout!=null){
|
||||
int color;
|
||||
if(isAtTop){
|
||||
color=UiUtils.getThemeColor(context, R.attr.toolbarBackground);
|
||||
}else{
|
||||
color=UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.toolbarBackground), UiUtils.getThemeColor(context, R.attr.colorWindowBackground), 0.07843137f);
|
||||
}
|
||||
anims.add(ObjectAnimator.ofArgb(fragmentRootLayout, "statusBarColor", color));
|
||||
}
|
||||
set.playTogether(anims);
|
||||
set.setDuration(150);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
currentPanelsAnim=null;
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
currentPanelsAnim=set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.os.LocaleList;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -38,4 +38,22 @@ public class StatusFilterPredicate implements Predicate<Status>{
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean testWithWarning(Status status) {
|
||||
if(status.filtered!=null){
|
||||
if (status.filtered.isEmpty()){
|
||||
return true;
|
||||
}
|
||||
boolean matches=status.filtered.stream()
|
||||
.map(filterResult->filterResult.filter)
|
||||
.filter(filter->filter.expiresAt==null||filter.expiresAt.isAfter(Instant.now()))
|
||||
.anyMatch(filter->filter.filterAction==Filter.FilterAction.WARN);
|
||||
return !matches;
|
||||
}
|
||||
for(Filter filter:filters){
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.joinmastodon.android.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.MatchResult;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
// not a good class
|
||||
public class StatusTextEncoder {
|
||||
private final Function<String, String> fn;
|
||||
|
||||
// see ComposeFragment.HIGHLIGHT_PATTERN
|
||||
private final static Pattern EXCLUDE_PATTERN = Pattern.compile("\\s*(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?|#([^\\s.]+))\\s*");
|
||||
|
||||
public StatusTextEncoder(Function<String, String> fn) {
|
||||
this.fn = fn;
|
||||
}
|
||||
|
||||
// prettiest method award winner 2023 [citation needed]
|
||||
public String encode(String content) {
|
||||
StringBuilder encodedString = new StringBuilder();
|
||||
// matches mentions and hashtags
|
||||
Matcher m = EXCLUDE_PATTERN.matcher(content);
|
||||
int previousEnd = 0;
|
||||
while (m.find()) {
|
||||
MatchResult res = m.toMatchResult();
|
||||
// everything before the match - do encode
|
||||
encodedString.append(fn.apply(content.substring(previousEnd, res.start())));
|
||||
previousEnd = res.end();
|
||||
// the match - do not encode
|
||||
encodedString.append(res.group());
|
||||
}
|
||||
// everything after the last match - do encode
|
||||
encodedString.append(fn.apply(content.substring(previousEnd)));
|
||||
return encodedString.toString();
|
||||
}
|
||||
|
||||
// prettiest almost-exact replica of a pretty function
|
||||
public String decode(String content, Pattern regex) {
|
||||
Matcher m = regex.matcher(content);
|
||||
StringBuilder decodedString = new StringBuilder();
|
||||
int previousEnd = 0;
|
||||
while (m.find()) {
|
||||
MatchResult res = m.toMatchResult();
|
||||
// everything before the match - do not decode
|
||||
decodedString.append(content.substring(previousEnd, res.start()));
|
||||
previousEnd = res.end();
|
||||
// the match - do decode
|
||||
decodedString.append(fn.apply(res.group()));
|
||||
}
|
||||
decodedString.append(content.substring(previousEnd));
|
||||
return decodedString.toString();
|
||||
}
|
||||
}
|
||||
5
mastodon/src/main/res/color/button_text_m3_tonal.xml
Normal file
5
mastodon/src/main/res/color/button_text_m3_tonal.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorM3OnSecondaryContainer" android:state_enabled="true"/>
|
||||
<item android:color="?colorM3OnSurface" android:alpha="0.38"/>
|
||||
</selector>
|
||||
19
mastodon/src/main/res/drawable/bg_button_m3_tonal.xml
Normal file
19
mastodon/src/main/res/drawable/bg_button_m3_tonal.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_enabled="true">
|
||||
<ripple android:color="@color/m3_on_secondary_container_overlay">
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?colorM3SecondaryContainer"/>
|
||||
<corners android:radius="20dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
</item>
|
||||
<item>
|
||||
<shape>
|
||||
<solid android:color="?colorM3DisabledBackground"/>
|
||||
<corners android:radius="20dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#E894000C"/>
|
||||
<corners android:radius="24dp"/>
|
||||
<solid android:color="#D49E050A"/>
|
||||
<corners android:radius="20dp"/>
|
||||
</shape>
|
||||
5
mastodon/src/main/res/drawable/bg_onboarding_avatar.xml
Normal file
5
mastodon/src/main/res/drawable/bg_onboarding_avatar.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="28dp"/>
|
||||
<solid android:color="?colorM3Surface"/>
|
||||
</shape>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<color android:color="?colorM3Background"/>
|
||||
<color android:color="?colorWindowBackground"/>
|
||||
</item>
|
||||
<item android:id="@+id/color_overlay">
|
||||
<color android:color="?colorM3Primary"/>
|
||||
<color android:color="?toolbarBackground"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
8
mastodon/src/main/res/drawable/bg_pill.xml
Normal file
8
mastodon/src/main/res/drawable/bg_pill.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:dither="true"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="4sp" />
|
||||
<solid android:color="?colorBackgroundLight" />
|
||||
<stroke android:width="2dp" android:color="#00ff00" />
|
||||
</shape>
|
||||
9
mastodon/src/main/res/drawable/ic_add_24px.xml
Normal file
9
mastodon/src/main/res/drawable/ic_add_24px.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11,19V13H5V11H11V5H13V11H19V13H13V19Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M29.45,6V9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39H39Q39,39 39,39Q39,39 39,39V18.6H42V39Q42,40.2 41.1,41.1Q40.2,42 39,42H9Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6ZM38,6V10.05H42.05V13.05H38V17.1H35V13.05H30.95V10.05H35V6ZM12,33.9H36L28.8,24.3L22.45,32.65L17.75,26.45ZM9,9V14.55V18.6V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/>
|
||||
</vector>
|
||||
9
mastodon/src/main/res/drawable/ic_drag_handle_24px.xml
Normal file
9
mastodon/src/main/res/drawable/ic_drag_handle_24px.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,15V13H20V15ZM4,11V9H20V11Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M16.25 5.18c-0.25 0.33-0.187 0.8 0.142 1.051C18.18 7.595 19.25 9.708 19.25 12c0 3.736-2.826 6.812-6.457 7.207l0.677-0.677c0.293-0.293 0.293-0.767 0-1.06-0.267-0.267-0.683-0.29-0.977-0.073L12.41 17.47l-2 2c-0.266 0.266-0.29 0.683-0.073 0.976l0.073 0.084 2 2c0.293 0.293 0.768 0.293 1.06 0 0.267-0.266 0.291-0.683 0.073-0.976L13.47 21.47l-0.75-0.75c4.495-0.365 8.03-4.13 8.03-8.72 0-2.765-1.292-5.317-3.448-6.961-0.33-0.252-0.8-0.188-1.051 0.141zm-5.72-3.71c-0.293 0.293-0.293 0.767 0 1.06l0.75 0.75C6.784 3.645 3.25 7.41 3.25 12c0 2.645 1.181 5.097 3.18 6.75 0.32 0.263 0.793 0.218 1.057-0.102 0.263-0.319 0.218-0.792-0.1-1.055-1.66-1.37-2.637-3.4-2.637-5.593 0-3.736 2.825-6.811 6.456-7.207L10.53 5.47c-0.293 0.293-0.293 0.767 0 1.06 0.293 0.293 0.768 0.293 1.061 0l2-2c0.293-0.293 0.293-0.767 0-1.06l-2-2c-0.293-0.293-0.768-0.293-1.06 0z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
|
||||
<path android:pathData="M15.794 7.733c0.286 0.3 0.274 0.774-0.026 1.06l-5.25 5.001c-0.29 0.276-0.745 0.276-1.035 0l-5.25-5c-0.3-0.287-0.312-0.761-0.026-1.061 0.286-0.3 0.76-0.312 1.06-0.026l4.734 4.509 4.733-4.51c0.3-0.285 0.774-0.273 1.06 0.027z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M4.293 8.293c0.39-0.39 1.024-0.39 1.414 0L12 14.586l6.293-6.293c0.39-0.39 1.024-0.39 1.414 0 0.39 0.39 0.39 1.024 0 1.414l-7 7c-0.39 0.39-1.024 0.39-1.414 0l-7-7c-0.39-0.39-0.39-1.024 0-1.414z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
|
||||
<path android:pathData="M3.26 11.602C3.942 8.327 6.793 6 10 6c3.206 0 6.057 2.327 6.74 5.602 0.057 0.27 0.322 0.444 0.593 0.387 0.27-0.056 0.443-0.32 0.387-0.591C16.943 7.673 13.693 5 10 5c-3.693 0-6.943 2.673-7.72 6.398-0.056 0.27 0.117 0.535 0.388 0.591 0.27 0.057 0.535-0.117 0.591-0.387zM10 8c-1.933 0-3.5 1.567-3.5 3.5S8.067 15 10 15s3.5-1.567 3.5-3.5S11.933 8 10 8zm-2.5 3.5C7.5 10.12 8.62 9 10 9s2.5 1.12 2.5 2.5S11.38 14 10 14s-2.5-1.12-2.5-2.5z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M2.75 18h12.5c0.414 0 0.75 0.335 0.75 0.75 0 0.38-0.282 0.693-0.648 0.743L15.25 19.5H2.75C2.336 19.5 2 19.164 2 18.75c0-0.38 0.282-0.694 0.648-0.744L2.75 18h12.5-12.5zm0-6.5h18.5c0.414 0 0.75 0.335 0.75 0.75 0 0.38-0.282 0.693-0.648 0.743L21.25 13H2.75C2.336 13 2 12.664 2 12.25c0-0.38 0.282-0.694 0.648-0.743L2.75 11.5h18.5-18.5zm0-6.497h15.5c0.414 0 0.75 0.336 0.75 0.75 0 0.38-0.282 0.693-0.648 0.743L18.25 6.503H2.75C2.336 6.503 2 6.167 2 5.753c0-0.38 0.282-0.694 0.648-0.743L2.75 5.003h15.5-15.5z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="28dp" android:height="28dp" android:viewportWidth="28" android:viewportHeight="28">
|
||||
<path android:pathData="M16.25 21c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75H3.75C3.336 22.5 3 22.164 3 21.75S3.336 21 3.75 21h12.5zm7.998-7.502c0.415 0 0.75 0.336 0.75 0.75s-0.335 0.75-0.75 0.75h-20.5c-0.414 0-0.75-0.336-0.75-0.75s0.336-0.75 0.75-0.75h20.5zM20.25 6C20.664 6 21 6.336 21 6.75S20.664 7.5 20.25 7.5H3.75C3.336 7.5 3 7.164 3 6.75S3.336 6 3.75 6h16.5z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
|
||||
<path android:pathData="M10 2c1.657 0 3 1.343 3 3v1h1c1.105 0 2 0.895 2 2v7c0 1.105-0.895 2-2 2H6c-1.105 0-2-0.895-2-2V8c0-1.105 0.895-2 2-2h1V5c0-1.657 1.343-3 3-3zm4 5H6C5.448 7 5 7.448 5 8v7c0 0.552 0.448 1 1 1h8c0.552 0 1-0.448 1-1V8c0-0.552-0.448-1-1-1zm-4 3.5c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM10 3C8.895 3 8 3.895 8 5v1h4V5c0-1.105-0.895-2-2-2z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M12 2c2.21 0 4 1.79 4 4v2h2.5C19.328 8 20 8.672 20 9.5v11c0 0.828-0.672 1.5-1.5 1.5h-13C4.672 22 4 21.328 4 20.5v-11C4 8.672 4.672 8 5.5 8H8V6c0-2.21 1.79-4 4-4zm0 11.5c-0.828 0-1.5 0.672-1.5 1.5s0.672 1.5 1.5 1.5c0.829 0 1.5-0.672 1.5-1.5s-0.671-1.5-1.5-1.5zM12 4c-1.105 0-2 0.895-2 2v2h4V6c0-1.105-0.895-2-2-2z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user